From fe7be9d69528751175cd082288074d921c426ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rodriguez?= Date: Wed, 18 Sep 2024 12:37:58 +0200 Subject: [PATCH] feat(auth): improve auth events (#746) * feat(auth): implement before and after logout events * doc: add logout events documentation * feat(auth): implement before and after login events * style: correct eslint errors * ci(docker): correct functional tests * ci(docker): correct documentation tests --- .ci/start_kuzzle.sh | 2 +- .ci/test-docs.sh | 6 +- doc/7/essentials/events/index.md | 32 +++++++- src/Kuzzle.ts | 18 ++++- src/controllers/Auth.ts | 50 ++++++++++--- src/core/KuzzleEventEmitter.ts | 4 + test/controllers/auth.test.js | 99 ++++++++++++++++++++++--- test/kuzzle/authenticator.test.js | 12 +-- test/kuzzle/listenersManagement.test.js | 4 + 9 files changed, 193 insertions(+), 34 deletions(-) diff --git a/.ci/start_kuzzle.sh b/.ci/start_kuzzle.sh index e193cb590..4a7a8789b 100755 --- a/.ci/start_kuzzle.sh +++ b/.ci/start_kuzzle.sh @@ -4,7 +4,7 @@ set -e echo "[$(date --rfc-3339 seconds)] - Start Kuzzle stack" -docker-compose -f .ci/docker-compose.yml up -d +docker compose -f .ci/docker-compose.yml up -d spinner="/" until $(curl --output /dev/null --silent --head --fail http://localhost:7512); do diff --git a/.ci/test-docs.sh b/.ci/test-docs.sh index 277049c54..8ff1d1ad8 100644 --- a/.ci/test-docs.sh +++ b/.ci/test-docs.sh @@ -5,7 +5,7 @@ set -ex here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" cd "$here" -docker-compose -f ./doc/docker-compose.yml pull -docker-compose -f ./doc/docker-compose.yml run doc-tests node index +docker compose -f ./doc/docker-compose.yml pull +docker compose -f ./doc/docker-compose.yml run doc-tests node index EXIT=$? -docker-compose -f ./doc/docker-compose.yml down +docker compose -f ./doc/docker-compose.yml down diff --git a/doc/7/essentials/events/index.md b/doc/7/essentials/events/index.md index 367e85b87..3e3a564e6 100644 --- a/doc/7/essentials/events/index.md +++ b/doc/7/essentials/events/index.md @@ -59,10 +59,19 @@ Triggered when the current session has been unexpectedly disconnected. | `websocket/auth-renewal` | The websocket protocol si reconnecting to renew the token. See [Websocket Cookie Authentication](sdk/js/7/protocols/websocket/introduction#cookie-authentication). | | `user/connection-closed` | The disconnection is done by the user. | | `network/error` | An network error occured and caused a disconnection. | -## loginAttempt + +## beforeLogin + +Triggered before login attempt. + +## afterLogin Triggered when a login attempt completes, either with a success or a failure result. +## loginAttempt + +Legacy event triggered when a login attempt completes, either with a success or a failure result. + **Callback arguments:** `@param {object} data` @@ -72,6 +81,27 @@ Triggered when a login attempt completes, either with a success or a failure res | `success` |
boolean
| Indicate if login attempt succeed | | `error` |
string
| Error message when login fail | +## beforeLogout + +Triggered before logout attempt. + +## afterLogout + +Triggered when a logout attempt completes, either with a success or a failure result. + +## logoutAttempt + +Legacy event triggered when a logout attempt completes, either with a success or a failure result. + +**Callback arguments:** + +`@param {object} data` + +| Property | Type | Description | +| --------- | ------------------ | --------------------------------- | +| `success` |
boolean
| Indicate if logout attempt succeed | +| `error` |
string
| Error message when logout fail | + ## networkError Triggered when the SDK has failed to connect to Kuzzle. diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index 5b45e9a18..be16f0785 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -84,7 +84,11 @@ export class Kuzzle extends KuzzleEventEmitter { "discarded", "disconnected", "loginAttempt", + "beforeLogin", + "afterLogin", "logoutAttempt", + "beforeLogout", + "afterLogout", "networkError", "offlineQueuePush", "offlineQueuePop", @@ -237,6 +241,7 @@ export class Kuzzle extends KuzzleEventEmitter { this.protocol = protocol; this._protectedEvents = { + afterLogin: {}, connected: {}, disconnected: {}, error: {}, @@ -350,7 +355,10 @@ export class Kuzzle extends KuzzleEventEmitter { this._loggedIn = false; - this.on("loginAttempt", async (status) => { + /** + * When successfuly logged in + */ + this.on("afterLogin", async (status) => { if (status.success) { this._loggedIn = true; return; @@ -369,7 +377,7 @@ export class Kuzzle extends KuzzleEventEmitter { /** * When successfuly logged out */ - this.on("logoutAttempt", (status) => { + this.on("afterLogout", (status) => { if (status.success) { this._loggedIn = false; } @@ -574,12 +582,14 @@ export class Kuzzle extends KuzzleEventEmitter { listener: () => void ): this; + on(eventName: "beforeLogout", listener: () => void): this; on( - eventName: "logoutAttempt", + eventName: "logoutAttempt" | "afterLogout", listener: (status: { success: true }) => void ): this; + on(eventName: "beforeLogin", listener: () => void): this; on( - eventName: "loginAttempt", + eventName: "loginAttempt" | "afterLogin", listener: (data: { success: boolean; error: string }) => void ): this; on(eventName: "discarded", listener: (request: RequestPayload) => void): this; diff --git a/src/controllers/Auth.ts b/src/controllers/Auth.ts index b8c0d99fa..8aedf6c2b 100644 --- a/src/controllers/Auth.ts +++ b/src/controllers/Auth.ts @@ -447,6 +447,7 @@ export class AuthController extends BaseController { strategy, }; + this.kuzzle.emit("beforeLogin"); return this.query(request, { queuable: false, timeout: -1, verb: "POST" }) .then((response) => { if (this.kuzzle.cookieAuthentication) { @@ -458,16 +459,22 @@ export class AuthController extends BaseController { error: err.message, success: false, }); + this.kuzzle.emit("afterLogin", { + error: err.message, + success: false, + }); throw err; } this.kuzzle.emit("loginAttempt", { success: true }); + this.kuzzle.emit("afterLogin", { success: true }); return; } this._authenticationToken = new Jwt(response.result.jwt); this.kuzzle.emit("loginAttempt", { success: true }); + this.kuzzle.emit("afterLogin", { success: true }); return response.result.jwt; }) @@ -476,6 +483,11 @@ export class AuthController extends BaseController { error: err.message, success: false, }); + this.kuzzle.emit("afterLogin", { + error: err.message, + success: false, + }); + throw err; }); } @@ -485,17 +497,37 @@ export class AuthController extends BaseController { * * @see https://docs.kuzzle.io/sdk/js/7/controllers/auth/logout */ - logout(): Promise { - return this.query( - { - action: "logout", - cookieAuth: this.kuzzle.cookieAuthentication, - }, - { queuable: false, timeout: -1 } - ).then(() => { + async logout(): Promise { + this.kuzzle.emit("beforeLogout"); + try { + await this.query( + { + action: "logout", + cookieAuth: this.kuzzle.cookieAuthentication, + }, + { queuable: false, timeout: -1 } + ); this._authenticationToken = null; + /** + * @deprecated logout `logoutAttempt` is legacy event. Use afterLogout instead. + */ this.kuzzle.emit("logoutAttempt", { success: true }); - }); + this.kuzzle.emit("afterLogout", { success: true }); + } catch (error) { + /** + * @deprecated logout `logoutAttempt` is legacy event. Use afterLogout instead. + */ + this.kuzzle.emit("logoutAttempt", { + error: (error as Error).message, + success: false, + }); + this.kuzzle.emit("afterLogout", { + error: (error as Error).message, + success: false, + }); + + throw error; + } } /** diff --git a/src/core/KuzzleEventEmitter.ts b/src/core/KuzzleEventEmitter.ts index 1196957f4..27608a853 100644 --- a/src/core/KuzzleEventEmitter.ts +++ b/src/core/KuzzleEventEmitter.ts @@ -16,7 +16,11 @@ export type PublicKuzzleEvents = | "discarded" | "disconnected" | "loginAttempt" + | "beforeLogin" + | "afterLogin" | "logoutAttempt" + | "beforeLogout" + | "afterLogout" | "networkError" | "offlineQueuePush" | "offlineQueuePop" diff --git a/test/controllers/auth.test.js b/test/controllers/auth.test.js index 11021a3f5..2909801d0 100644 --- a/test/controllers/auth.test.js +++ b/test/controllers/auth.test.js @@ -7,9 +7,20 @@ const { AuthController } = require("../../src/controllers/Auth"); const { User } = require("../../src/core/security/User"); const generateJwt = require("../mocks/generateJwt.mock"); +/** + * Kuzzle interface + * + * @typedef {Object} Kuzzle + * @property {import("sinon").SinonStub} Kuzzle.query + * @property {import("sinon").SinonStub} Kuzzle.emit + * @property {boolean} Kuzzle.cookieAuthentication + */ + describe("Auth Controller", () => { const options = { opt: "in" }; - let jwt, kuzzle; + /** @type {Kuzzle} */ + let kuzzle; + let jwt; beforeEach(() => { kuzzle = new KuzzleEventEmitter(); @@ -495,17 +506,33 @@ describe("Auth Controller", () => { }); }); - it('should trigger a "loginAttempt" event once the user is logged in', () => { + it("should trigger a login events the user is logged in", async () => { kuzzle.emit = sinon.stub(); - return kuzzle.auth + await kuzzle.auth.login("strategy", credentials, "expiresIn").then(() => { + should(kuzzle.emit).be.calledWith("beforeLogin"); + should(kuzzle.emit).be.calledWith("afterLogin", { success: true }); + should(kuzzle.emit).be.calledWith("loginAttempt", { success: true }); + }); + kuzzle.emit.reset(); + + kuzzle.query.rejects(); + await kuzzle.auth .login("strategy", credentials, "expiresIn") - .then(() => { - should(kuzzle.emit).be.calledOnce().be.calledWith("loginAttempt"); + .catch(() => { + should(kuzzle.emit).be.calledWith("beforeLogin"); + should(kuzzle.emit).be.calledWith("afterLogin", { + success: false, + error: "Error", + }); + should(kuzzle.emit).be.calledWith("loginAttempt", { + success: false, + error: "Error", + }); }); }); - it('should trigger a "loginAttempt" event once the user is logged in with cookieAuthentication enabled', () => { + it("should trigger a login events the user is logged in with cookieAuthentication enabled", async () => { kuzzle.emit = sinon.stub(); kuzzle.cookieAuthentication = true; kuzzle.query.resolves({ @@ -514,10 +541,26 @@ describe("Auth Controller", () => { }, }); - return kuzzle.auth - .login("strategy", credentials, "expiresIn") - .then(() => { - should(kuzzle.emit).be.calledOnce().be.calledWith("loginAttempt"); + await kuzzle.auth.login("strategy", credentials, "expiresIn").then(() => { + should(kuzzle.emit).be.calledWith("beforeLogin"); + should(kuzzle.emit).be.calledWith("afterLogin", { success: true }); + should(kuzzle.emit).be.calledWith("loginAttempt", { success: true }); + }); + kuzzle.emit.reset(); + + kuzzle.query.rejects(); + await should(kuzzle.auth.login("strategy", credentials, "expiresIn")) + .be.rejected() + .catch(() => { + should(kuzzle.emit).be.calledWith("beforeLogin"); + should(kuzzle.emit).be.calledWith("afterLogin", { + success: false, + error: "Error", + }); + should(kuzzle.emit).be.calledWith("loginAttempt", { + success: false, + error: "Error", + }); }); }); @@ -570,6 +613,42 @@ describe("Auth Controller", () => { should(kuzzle.auth.authenticationToken).be.null(); }); }); + + // ? Legacy event + it('should trigger a legacy "logoutAttempt" event the user is logged out', async () => { + kuzzle.emit = sinon.stub(); + await kuzzle.auth.logout().then(() => { + should(kuzzle.emit).be.calledWith("logoutAttempt", { success: true }); + }); + kuzzle.emit.reset(); + + // ? Fail logout + kuzzle.query.rejects(); + await kuzzle.auth.logout().catch(() => { + should(kuzzle.emit).be.calledWith("logoutAttempt", { + success: false, + error: "Error", + }); + }); + }); + + it("should trigger logout events when the user is logged out", async () => { + kuzzle.emit = sinon.stub(); + await kuzzle.auth.logout().then(() => { + should(kuzzle.emit).be.calledWith("beforeLogout"); + should(kuzzle.emit).be.calledWith("afterLogout", { success: true }); + }); + kuzzle.emit.reset(); + + // ? Fail logout + kuzzle.query.rejects(); + await kuzzle.auth.logout().catch(() => { + should(kuzzle.emit).be.calledWith("afterLogout", { + success: false, + error: "Error", + }); + }); + }); }); describe("#updateMyCredentials", () => { diff --git a/test/kuzzle/authenticator.test.js b/test/kuzzle/authenticator.test.js index 488b80e07..8b68eaa7c 100644 --- a/test/kuzzle/authenticator.test.js +++ b/test/kuzzle/authenticator.test.js @@ -59,7 +59,7 @@ describe("Kuzzle authenticator function mecanisms", () => { }); }); - describe("loginAttempt listener", () => { + describe("login listener", () => { let resolve; let promise; @@ -73,7 +73,7 @@ describe("Kuzzle authenticator function mecanisms", () => { }); should(kuzzle._loggedIn).be.false(); - kuzzle.emit("loginAttempt", { success: true }); + kuzzle.emit("afterLogin", { success: true }); setTimeout(() => { should(kuzzle.auth.checkToken).not.be.calledOnce(); @@ -92,7 +92,7 @@ describe("Kuzzle authenticator function mecanisms", () => { kuzzle.auth.checkToken.resolves({ valid: true }); should(kuzzle._loggedIn).be.false(); - kuzzle.emit("loginAttempt", { success: false, err: new Error("foo") }); + kuzzle.emit("afterLogin", { success: false, err: new Error("foo") }); setTimeout(() => { should(kuzzle.auth.checkToken).be.calledOnce(); @@ -104,17 +104,17 @@ describe("Kuzzle authenticator function mecanisms", () => { }); }); - describe("logoutAttempt listener", () => { + describe("logout listener", () => { let resolve; let promise; - it("should set _loggedIn to false on logout", async () => { + it("should set _loggedIn to false on afterLogout", async () => { promise = new Promise((_resolve) => { resolve = _resolve; }); kuzzle._loggedIn = true; - kuzzle.emit("logoutAttempt", { success: true }); + kuzzle.emit("afterLogout", { success: true }); setTimeout(() => { should(kuzzle.auth.checkToken).not.be.calledOnce(); diff --git a/test/kuzzle/listenersManagement.test.js b/test/kuzzle/listenersManagement.test.js index efffcd7e5..35c071d26 100644 --- a/test/kuzzle/listenersManagement.test.js +++ b/test/kuzzle/listenersManagement.test.js @@ -26,7 +26,11 @@ describe("Kuzzle listeners management", () => { "discarded", "disconnected", "loginAttempt", + "beforeLogin", + "afterLogin", "logoutAttempt", + "beforeLogout", + "afterLogout", "networkError", "offlineQueuePush", "offlineQueuePop",