Skip to content

Commit

Permalink
feat(auth): improve auth events (#746)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sebtiz13 authored Sep 18, 2024
1 parent ab62885 commit fe7be9d
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .ci/start_kuzzle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .ci/test-docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 31 additions & 1 deletion doc/7/essentials/events/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -72,6 +81,27 @@ Triggered when a login attempt completes, either with a success or a failure res
| `success` | <pre>boolean</pre> | Indicate if login attempt succeed |
| `error` | <pre>string</pre> | 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` | <pre>boolean</pre> | Indicate if logout attempt succeed |
| `error` | <pre>string</pre> | Error message when logout fail |

## networkError

Triggered when the SDK has failed to connect to Kuzzle.
Expand Down
18 changes: 14 additions & 4 deletions src/Kuzzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ export class Kuzzle extends KuzzleEventEmitter {
"discarded",
"disconnected",
"loginAttempt",
"beforeLogin",
"afterLogin",
"logoutAttempt",
"beforeLogout",
"afterLogout",
"networkError",
"offlineQueuePush",
"offlineQueuePop",
Expand Down Expand Up @@ -237,6 +241,7 @@ export class Kuzzle extends KuzzleEventEmitter {
this.protocol = protocol;

this._protectedEvents = {
afterLogin: {},
connected: {},
disconnected: {},
error: {},
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
50 changes: 41 additions & 9 deletions src/controllers/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
})
Expand All @@ -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;
});
}
Expand All @@ -485,17 +497,37 @@ export class AuthController extends BaseController {
*
* @see https://docs.kuzzle.io/sdk/js/7/controllers/auth/logout
*/
logout(): Promise<void> {
return this.query(
{
action: "logout",
cookieAuth: this.kuzzle.cookieAuthentication,
},
{ queuable: false, timeout: -1 }
).then(() => {
async logout(): Promise<void> {
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;
}
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/core/KuzzleEventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export type PublicKuzzleEvents =
| "discarded"
| "disconnected"
| "loginAttempt"
| "beforeLogin"
| "afterLogin"
| "logoutAttempt"
| "beforeLogout"
| "afterLogout"
| "networkError"
| "offlineQueuePush"
| "offlineQueuePop"
Expand Down
99 changes: 89 additions & 10 deletions test/controllers/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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({
Expand All @@ -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",
});
});
});

Expand Down Expand Up @@ -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", () => {
Expand Down
Loading

0 comments on commit fe7be9d

Please sign in to comment.