Skip to content

Commit

Permalink
feat: remove seed from internal storage if we have it from config
Browse files Browse the repository at this point in the history
  • Loading branch information
Juiced66 committed Oct 30, 2024
1 parent 441bbc8 commit 2467201
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 54 deletions.
35 changes: 28 additions & 7 deletions lib/core/security/tokenRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,24 @@ export class TokenRepository extends ObjectRepository<Token> {
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);
}
}
}
}

/**
Expand Down Expand Up @@ -189,8 +207,10 @@ export class TokenRepository extends ObjectRepository<Token> {
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,
Expand All @@ -211,7 +231,8 @@ export class TokenRepository extends ObjectRepository<Token> {
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 &&
Expand Down Expand Up @@ -318,14 +339,14 @@ export class TokenRepository extends ObjectRepository<Token> {
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);
}

Expand Down
35 changes: 18 additions & 17 deletions lib/kuzzle/internalIndexHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class InternalIndexHandler extends Store {
const bootstrapped = await this.exists("config", this._BOOTSTRAP_DONE_ID);

if (bootstrapped) {
await this._initSecret();
return;
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
}

Expand Down
3 changes: 0 additions & 3 deletions lib/kuzzle/kuzzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
77 changes: 51 additions & 26 deletions test/kuzzle/internalIndexHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ describe("#kuzzle/InternalIndexHandler", () => {

internalIndexHandler = new InternalIndexHandler();

sinon.stub(internalIndexHandler, "_initSecret").resolves();

await internalIndexHandler.init();

should(kuzzle.ask).calledWith(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -309,7 +313,7 @@ describe("#kuzzle/InternalIndexHandler", () => {
});
});

describe("#_persistSecret", () => {
describe("#_initSecret", () => {
const randomBytesMock = sinon.stub().returns(Buffer.from("12345"));

before(() => {
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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);
});
});
});
1 change: 0 additions & 1 deletion test/mocks/internalIndexHandler.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand Down

0 comments on commit 2467201

Please sign in to comment.