From 24672019074a30384bcfa7cdc6af148e39338ef3 Mon Sep 17 00:00:00 2001 From: Juiced66 Date: Tue, 15 Oct 2024 09:55:19 +0200 Subject: [PATCH] feat: remove seed from internal storage if we have it from config --- lib/core/security/tokenRepository.ts | 35 ++++++++--- lib/kuzzle/internalIndexHandler.js | 35 +++++------ lib/kuzzle/kuzzle.ts | 3 - test/kuzzle/internalIndexHandler.test.js | 77 ++++++++++++++++-------- test/mocks/internalIndexHandler.mock.js | 1 - 5 files changed, 97 insertions(+), 54 deletions(-) diff --git a/lib/core/security/tokenRepository.ts b/lib/core/security/tokenRepository.ts index 9c9c1fdeb0..b5ce8de283 100644 --- a/lib/core/security/tokenRepository.ts +++ b/lib/core/security/tokenRepository.ts @@ -136,6 +136,24 @@ export class TokenRepository extends ObjectRepository { global.kuzzle.onAsk("core:security:token:verify", (hash) => this.verifyToken(hash), ); + + // ? those checks are necessary to detect JWT seed changes and delete existing token if necessary + const existingTokens = await global.kuzzle.ask( + "core:cache:internal:searchKeys", + "repos/kuzzle/token/*", + ); + + if (existingTokens.length > 0) { + try { + const [, token] = existingTokens[0].split("#"); + await this.verifyToken(token); + } catch (e) { + // ? seed has changed + if (e.id === "security.token.invalid") { + await global.kuzzle.ask("core:cache:internal:del", existingTokens); + } + } + } } /** @@ -189,8 +207,10 @@ export class TokenRepository extends ObjectRepository { async generateToken( user: User, { - algorithm = global.kuzzle.config.security.jwt.algorithm, - expiresIn = global.kuzzle.config.security.jwt.expiresIn, + algorithm = global.kuzzle.config.security.authToken.algorithm ?? + global.kuzzle.config.security.jwt.algorithm, + expiresIn = global.kuzzle.config.security.authToken.expiresIn ?? + global.kuzzle.config.security.jwt.expiresIn, bypassMaxTTL = false, type = "authToken", singleUse = false, @@ -211,7 +231,8 @@ export class TokenRepository extends ObjectRepository { const maxTTL = type === "apiKey" ? global.kuzzle.config.security.apiKey.maxTTL - : global.kuzzle.config.security.jwt.maxTTL; + : global.kuzzle.config.security.authToken.maxTTL ?? + global.kuzzle.config.security.jwt.maxTTL; if ( !bypassMaxTTL && @@ -318,14 +339,14 @@ export class TokenRepository extends ObjectRepository { throw new jwt.JsonWebTokenError("Invalid token"); } } catch (err) { - if (err instanceof jwt.TokenExpiredError) { - throw securityError.get("expired"); - } - if (err instanceof jwt.JsonWebTokenError) { throw securityError.get("invalid"); } + if (err instanceof jwt.TokenExpiredError) { + throw securityError.get("expired"); + } + throw securityError.getFrom(err, "verification_error", err.message); } diff --git a/lib/kuzzle/internalIndexHandler.js b/lib/kuzzle/internalIndexHandler.js index e944284f86..72358e8378 100644 --- a/lib/kuzzle/internalIndexHandler.js +++ b/lib/kuzzle/internalIndexHandler.js @@ -111,6 +111,7 @@ class InternalIndexHandler extends Store { const bootstrapped = await this.exists("config", this._BOOTSTRAP_DONE_ID); if (bootstrapped) { + await this._initSecret(); return; } @@ -150,7 +151,7 @@ class InternalIndexHandler extends Store { await this.createInitialValidations(); debug("Bootstrapping JWT secret"); - await this._persistSecret(); + await this._initSecret(); // Create datamodel version await this.create( @@ -202,24 +203,24 @@ class InternalIndexHandler extends Store { await Bluebird.all(promises); } - async getSecret() { - const response = await this.get("config", this._JWT_SECRET_ID); + async _initSecret() { + const { authToken, jwt } = global.kuzzle.config.security; + const configSeed = + authToken && authToken.secret ? authToken.secret : jwt && jwt.secret; - return response._source.seed; - } - - async _persistSecret() { - const seed = - global.kuzzle.config.security.jwt.secret || - crypto.randomBytes(512).toString("hex"); + let storedSeed = await this.exists("config", this._JWT_SECRET_ID); - await this.create( - "config", - { seed }, - { - id: this._JWT_SECRET_ID, - }, - ); + if (!configSeed && !storedSeed) { + storedSeed = crypto.randomBytes(512).toString("hex"); + await this.create( + "config", + { seed: storedSeed }, + { id: this._JWT_SECRET_ID }, + ); + } + global.kuzzle.secret = configSeed + ? configSeed + : (await this.get("config", this._JWT_SECRET_ID))._source.seed; } } diff --git a/lib/kuzzle/kuzzle.ts b/lib/kuzzle/kuzzle.ts index 683c1701ae..937b45d6a8 100644 --- a/lib/kuzzle/kuzzle.ts +++ b/lib/kuzzle/kuzzle.ts @@ -259,9 +259,6 @@ class Kuzzle extends KuzzleEventEmitter { // This will init the cluster module if enabled this.id = await this.initKuzzleNode(); - // Secret used to generate JWTs - this.secret = await this.internalIndex.getSecret(); - this.vault = vault.load(options.vaultKey, options.secretsFile); await this.validation.init(); diff --git a/test/kuzzle/internalIndexHandler.test.js b/test/kuzzle/internalIndexHandler.test.js index 1b68a85e52..ae6080bb9e 100644 --- a/test/kuzzle/internalIndexHandler.test.js +++ b/test/kuzzle/internalIndexHandler.test.js @@ -52,6 +52,8 @@ describe("#kuzzle/InternalIndexHandler", () => { internalIndexHandler = new InternalIndexHandler(); + sinon.stub(internalIndexHandler, "_initSecret").resolves(); + await internalIndexHandler.init(); should(kuzzle.ask).calledWith( @@ -113,9 +115,11 @@ describe("#kuzzle/InternalIndexHandler", () => { kuzzle.ask.withArgs("core:storage:private:document:exist").resolves(true); sinon.stub(internalIndexHandler, "_bootstrapSequence").resolves(); + sinon.stub(internalIndexHandler, "_initSecret").resolves(); await internalIndexHandler.init(); + should(internalIndexHandler._initSecret).called(); should(internalIndexHandler._bootstrapSequence).not.called(); should(kuzzle.ask).not.calledWith( @@ -174,13 +178,13 @@ describe("#kuzzle/InternalIndexHandler", () => { it("should trigger a complete bootstrap of the internal structures", async () => { sinon.stub(internalIndexHandler, "createInitialSecurities"); sinon.stub(internalIndexHandler, "createInitialValidations"); - sinon.stub(internalIndexHandler, "_persistSecret"); + sinon.stub(internalIndexHandler, "_initSecret"); await internalIndexHandler._bootstrapSequence(); should(internalIndexHandler.createInitialSecurities).called(); should(internalIndexHandler.createInitialValidations).called(); - should(internalIndexHandler._persistSecret).called(); + should(internalIndexHandler._initSecret).called(); should(kuzzle.ask).calledWith( "core:storage:private:document:create", @@ -309,7 +313,7 @@ describe("#kuzzle/InternalIndexHandler", () => { }); }); - describe("#_persistSecret", () => { + describe("#_initSecret", () => { const randomBytesMock = sinon.stub().returns(Buffer.from("12345")); before(() => { @@ -332,10 +336,22 @@ describe("#kuzzle/InternalIndexHandler", () => { it("should use the configured seed, if one is present", async () => { kuzzle.config.security.jwt.secret = "foobar"; + kuzzle.ask + .withArgs( + "core:storage:private:document:get", + internalIndexName, + "config", + internalIndexHandler._JWT_SECRET_ID, + ) + .resolves({ + _source: { + seed: "foobar", + }, + }); - await internalIndexHandler._persistSecret(); + await internalIndexHandler._initSecret(); - should(kuzzle.ask).calledWith( + should(kuzzle.ask).not.calledWith( "core:storage:private:document:create", internalIndexName, "config", @@ -347,7 +363,35 @@ describe("#kuzzle/InternalIndexHandler", () => { }); it("should auto-generate a new random seed if none is present in the config file", async () => { - await internalIndexHandler._persistSecret(); + kuzzle.ask + .withArgs( + "core:storage:private:document:exists", + internalIndexName, + "config", + internalIndexHandler._JWT_SECRET_ID, + ) + .resolves(false); + kuzzle.ask + .withArgs( + "core:storage:private:document:create", + internalIndexName, + "config", + internalIndexHandler._JWT_SECRET_ID, + ) + .resolves(); + kuzzle.ask + .withArgs( + "core:storage:private:document:get", + internalIndexName, + "config", + internalIndexHandler._JWT_SECRET_ID, + ) + .resolves({ + _source: { + seed: randomBytesMock().toString("hex"), + }, + }); + await internalIndexHandler._initSecret(); should(kuzzle.ask).calledWith( "core:storage:private:document:create", @@ -365,26 +409,7 @@ describe("#kuzzle/InternalIndexHandler", () => { kuzzle.ask.withArgs("core:storage:private:document:create").rejects(err); - return should(internalIndexHandler._persistSecret()).rejectedWith(err); - }); - }); - - describe("#getSecret", () => { - it("should fetch the secret seed from the storage space", async () => { - kuzzle.ask.withArgs("core:storage:private:document:get").resolves({ - _source: { - seed: "foobar", - }, - }); - - await should(internalIndexHandler.getSecret()).fulfilledWith("foobar"); - - should(kuzzle.ask).calledWith( - "core:storage:private:document:get", - internalIndexName, - "config", - internalIndexHandler._JWT_SECRET_ID, - ); + return should(internalIndexHandler._initSecret()).rejectedWith(err); }); }); }); diff --git a/test/mocks/internalIndexHandler.mock.js b/test/mocks/internalIndexHandler.mock.js index dd27ca740a..9b75fb023a 100644 --- a/test/mocks/internalIndexHandler.mock.js +++ b/test/mocks/internalIndexHandler.mock.js @@ -11,7 +11,6 @@ class InternalIndexHandlerMock extends InternalIndexHandler { sinon.stub(this, "init"); sinon.stub(this, "createInitialSecurities").resolves(); sinon.stub(this, "createInitialValidations").resolves(); - sinon.stub(this, "getSecret").resolves(); } }