Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login and Logout handler #3366

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
26 changes: 26 additions & 0 deletions e2e/node/e2e-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,29 @@ describe("Session events", () => {
expect(expiredFunc).toHaveBeenCalledTimes(1);
});
});

describe("New combined login and logout session event", () => {
zg009 marked this conversation as resolved.
Show resolved Hide resolved
jest.setTimeout(15 * 60 * 1000);

let session: Session;
let loginAndLogoutFunc: () => void;
let expiredFunc: () => void;

beforeEach(async () => {
session = new Session();
loginAndLogoutFunc = jest.fn();
expiredFunc = jest.fn();
session.events.on(EVENTS.LOGIN_AND_LOGOUT, loginAndLogoutFunc);
session.events.on(EVENTS.SESSION_EXPIRED, expiredFunc);

await session.login(getCredentials());
});

it("tests to make sure the function is called during both login and logout", async () => {
expect(session.info.isLoggedIn).toBe(true);
await session.logout();

expect(loginAndLogoutFunc).toHaveBeenCalledTimes(2);
expect(expiredFunc).toHaveBeenCalledTimes(0);
})
})
41 changes: 41 additions & 0 deletions packages/browser/src/Session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,47 @@ describe("Session", () => {
});
});

describe("login and logout", () => {
it("calls the registered callback on login and logout", async () => {
const myCallback = jest.fn();
const clientAuthentication = mockClientAuthentication();
const mySession = new Session({
clientAuthentication,
});

clientAuthentication.handleIncomingRedirect = jest
.fn<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
mockLocalStorage({});

mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback);
await mySession.handleIncomingRedirect("https://some.url");
expect(myCallback).toHaveBeenCalledTimes(1);
await mySession.logout();
expect(myCallback).toHaveBeenCalledTimes(2);

})

it("does not call the registered callback if login isn't successful", async () => {
const myCallback = jest.fn();
const clientAuthentication = mockClientAuthentication();
clientAuthentication.handleIncomingRedirect = jest
.fn<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
const mySession = new Session({ clientAuthentication });
mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback);
expect(myCallback).not.toHaveBeenCalled();
});
})

describe("sessionRestore", () => {
it("calls the registered callback on session restore", async () => {
// Set our window's location to our test value.
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener {
this.info.isLoggedIn = false;
if (emitSignal) {
(this.events as EventEmitter).emit(EVENTS.LOGOUT);
(this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT);
zg009 marked this conversation as resolved.
Show resolved Hide resolved
}
};

Expand Down Expand Up @@ -349,6 +350,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener {
// The login event can only be triggered **after** the user has been
// redirected from the IdP with access and ID tokens.
(this.events as EventEmitter).emit(EVENTS.LOGIN);
(this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT);
} else {
// If an URL is stored in local storage, we are being logged in after a
// silent authentication, so remove our currently stored URL location
Expand Down
50 changes: 49 additions & 1 deletion packages/core/src/SessionEventListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ type FALLBACK_ARGS = {
// Prevents from using a SessionEventEmitter as an aritrary EventEmitter.
listener: never;
};

type LOGIN_AND_LOGOUT_ARGS = {
eventName: typeof EVENTS.LOGIN_AND_LOGOUT;
listener: () => void;
}
export interface ISessionEventListener extends EventEmitter {
/**
* Register a listener called on successful login.
Expand All @@ -77,6 +80,15 @@ export interface ISessionEventListener extends EventEmitter {
eventName: LOGOUT_ARGS["eventName"],
listener: LOGOUT_ARGS["listener"],
): this;
/**
* Register a listener called on a successful login or logout.
* @param eventName The login and logout event name.
* @param listener The callback called on a successful login and logout.
*/
on(
eventName: LOGIN_AND_LOGOUT_ARGS["eventName"],
listener: LOGIN_AND_LOGOUT_ARGS["listener"]
): this;
/**
* Register a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -159,6 +171,15 @@ export interface ISessionEventListener extends EventEmitter {
eventName: LOGOUT_ARGS["eventName"],
listener: LOGOUT_ARGS["listener"],
): this;
/**
* Register a listener called on a successful login or logout.
* @param eventName The login and logout event name.
* @param listener The callback called on a successful login or logout.
*/
addListener(
eventName: LOGIN_AND_LOGOUT_ARGS["eventName"],
listener: LOGIN_AND_LOGOUT_ARGS["listener"]
): this;
/**
* Register a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -241,6 +262,15 @@ export interface ISessionEventListener extends EventEmitter {
eventName: LOGOUT_ARGS["eventName"],
listener: LOGOUT_ARGS["listener"],
): this;
/**
* Register a listener called on the next successful login or logout.
* @param eventName The login and logout event name.
* @param listener The callback called on the next successful login or logout.
*/
once(
eventName: LOGIN_AND_LOGOUT_ARGS["eventName"],
listener: LOGIN_AND_LOGOUT_ARGS["listener"]
): this;
/**
* Register a listener called on the next session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -324,6 +354,15 @@ export interface ISessionEventListener extends EventEmitter {
eventName: LOGOUT_ARGS["eventName"],
listener: LOGOUT_ARGS["listener"],
): this;
/**
* Unregister a listener called on a successful login or logout.
* @param eventName The login and logout event name.
* @param listener The callback to unregister.
*/
off(
eventName: LOGIN_AND_LOGOUT_ARGS["eventName"],
listener: LOGIN_AND_LOGOUT_ARGS["listener"]
): this;
/**
* Unegister a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down Expand Up @@ -405,6 +444,15 @@ export interface ISessionEventListener extends EventEmitter {
eventName: LOGOUT_ARGS["eventName"],
listener: LOGOUT_ARGS["listener"],
): this;
/**
* Unregister a listener called on a successful login or logout.
* @param eventName The login and logout event name
* @param listener The callback to unregister
*/
removeListener(
eventName: LOGIN_AND_LOGOUT_ARGS["eventName"],
listener: LOGIN_AND_LOGOUT_ARGS["listener"]
): this;
/**
* Unegister a listener called on session expiration.
* @param eventName The session expiration event name.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const EVENTS = {
ERROR: "error",
LOGIN: "login",
LOGOUT: "logout",
LOGIN_AND_LOGOUT: "loginAndLogout",
zg009 marked this conversation as resolved.
Show resolved Hide resolved
NEW_REFRESH_TOKEN: "newRefreshToken",
SESSION_EXPIRED: "sessionExpired",
SESSION_EXTENDED: "sessionExtended",
Expand Down
65 changes: 63 additions & 2 deletions packages/node/src/Session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,9 +488,28 @@ describe("Session", () => {
const mySession = new Session({ clientAuthentication });
mySession.events.on(EVENTS.LOGIN, myCallback);
await mySession.handleIncomingRedirect("https://some.url");
expect(myCallback).toHaveBeenCalled();
expect(myCallback).toHaveBeenCalledTimes(1);
});

it("does not call the registered cb on logout", async () => {
const myCallback = jest.fn();
const clientAuthentication = mockClientAuthentication();
clientAuthentication.handleIncomingRedirect = jest
.fn<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
const mySession = new Session({ clientAuthentication });
mySession.events.on(EVENTS.LOGIN, myCallback);
await mySession.handleIncomingRedirect("https://some.url");
expect(myCallback).toHaveBeenCalledTimes(1);

await mySession.logout();
expect(myCallback).toHaveBeenCalledTimes(1);
})

it("does not call the registered callback if login isn't successful", async () => {
const myCallback = jest.fn();
const clientAuthentication = mockClientAuthentication();
Expand Down Expand Up @@ -536,10 +555,52 @@ describe("Session", () => {

mySession.events.on(EVENTS.LOGOUT, myCallback);
await mySession.logout();
expect(myCallback).toHaveBeenCalled();
expect(myCallback).toHaveBeenCalledTimes(1);
});

});

describe("login and logout", () => {
it("calls the registered callback on login and logout", async () => {
const myCallback = jest.fn();
const clientAuthentication = mockClientAuthentication();
const mySession = new Session({
clientAuthentication,
});

clientAuthentication.handleIncomingRedirect = jest
.fn<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});

mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback)
await mySession.handleIncomingRedirect("https://some.url");
expect(myCallback).toHaveBeenCalledTimes(1);

await mySession.logout();
expect(myCallback).toHaveBeenCalledTimes(2);
})

it("does not call the registered callback if login isn't successful", async () => {
const myCallback = jest.fn();
const clientAuthentication = mockClientAuthentication();
clientAuthentication.handleIncomingRedirect = jest
.fn<ClientAuthentication["handleIncomingRedirect"]>()
.mockResolvedValue({
isLoggedIn: true,
sessionId: "a session ID",
webId: "https://some.webid#them",
});
const mySession = new Session({ clientAuthentication });
mySession.events.on(EVENTS.LOGIN_AND_LOGOUT, myCallback);

expect(myCallback).not.toHaveBeenCalled();
});
})

describe("sessionExpired", () => {
it("calls the provided callback when receiving the appropriate event", async () => {
const myCallback = jest.fn();
Expand Down
4 changes: 4 additions & 0 deletions packages/node/src/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener {
this.events.on(EVENTS.SESSION_EXPIRED, () => this.internalLogout(false));
}


/**
* Triggers the login process. Note that this method will redirect the user away from your app.
*
Expand All @@ -215,6 +216,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener {
if (loginInfo?.isLoggedIn) {
// Send a signal on successful client credentials login.
(this.events as EventEmitter).emit(EVENTS.LOGIN);
(this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT);
}
};

Expand Down Expand Up @@ -284,6 +286,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener {
this.info.isLoggedIn = false;
if (emitEvent) {
(this.events as EventEmitter).emit(EVENTS.LOGOUT);
(this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT);
}
};

Expand Down Expand Up @@ -320,6 +323,7 @@ export class Session extends EventEmitter implements IHasSessionEventListener {
// The login event can only be triggered **after** the user has been
// redirected from the IdP with access and ID tokens.
(this.events as EventEmitter).emit(EVENTS.LOGIN);
(this.events as EventEmitter).emit(EVENTS.LOGIN_AND_LOGOUT);
}
}
} finally {
Expand Down