From 05df9e5600cec351b1cacdd185520a87716a2450 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 5 May 2022 14:11:25 -0700 Subject: [PATCH 1/7] feature: adds a default callback to the idleManager By default, refreshes the page unless an alternative CB is provided during initialization or is registered after the fact --- packages/auth-client/src/idleManager.test.ts | 33 ++++++++++++++++++++ packages/auth-client/src/idleManager.ts | 17 ++++++++-- packages/auth-client/src/index.ts | 2 ++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/auth-client/src/idleManager.test.ts b/packages/auth-client/src/idleManager.test.ts index fa1d55a9c..8ad844c6c 100644 --- a/packages/auth-client/src/idleManager.test.ts +++ b/packages/auth-client/src/idleManager.test.ts @@ -2,6 +2,13 @@ import IdleManager from './idleManager'; jest.useFakeTimers(); +const { location } = window; + +beforeEach(() => { + delete window.location; + window.location = { reload: jest.fn() }; +}); + describe('IdleManager tests', () => { it('should call its callback after time spent inactive', () => { const cb = jest.fn(); @@ -12,6 +19,32 @@ describe('IdleManager tests', () => { expect(cb).toHaveBeenCalled(); manager.exit(); }); + it('should automatically reload the page if no other idle callback is registered', () => { + IdleManager.create(); + + expect(window.location.reload).not.toHaveBeenCalled(); + // simulate user being inactive for 10 minutes + jest.advanceTimersByTime(10 * 60 * 1000); + expect(window.location.reload).toHaveBeenCalled(); + }); + it('should replace the default callback if a callback is passed during creation', () => { + IdleManager.create({ onIdle: jest.fn() }); + + expect(window.location.reload).not.toHaveBeenCalled(); + // simulate user being inactive for 10 minutes + jest.advanceTimersByTime(10 * 60 * 1000); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + it('should replace the default callback if a callback is registered', () => { + const manager = IdleManager.create(); + + manager.registerCallback(jest.fn()); + + expect(window.location.reload).not.toHaveBeenCalled(); + // simulate user being inactive for 10 minutes + jest.advanceTimersByTime(10 * 60 * 1000); + expect(window.location.reload).not.toHaveBeenCalled(); + }); it('should delay allow configuration of the timeout', () => { const cb = jest.fn(); const extraDelay = 100; diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index 08f71a7b4..e14a004cc 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -32,6 +32,7 @@ class IdleManager { callbacks: IdleCB[] = []; idleTimeout: IdleManagerOptions['idleTimeout'] = 30 * 60 * 1000; timeoutID?: number = undefined; + private _usingDefaultCb = false; /** * Creates an {@link IdleManager} @@ -75,9 +76,17 @@ class IdleManager { */ protected constructor(options: IdleManagerOptions = {}) { const { onIdle, idleTimeout = 10 * 60 * 1000 } = options || {}; - this.callbacks = onIdle ? [onIdle] : []; + + const defaultIdleCB = () => { + window.location.reload(); + }; + + this.callbacks = onIdle ? [onIdle] : [defaultIdleCB]; this.idleTimeout = idleTimeout; + // If no callback is passed, set flag that we are using the default callback + this._usingDefaultCb = !onIdle; + const _resetTimer = this._resetTimer.bind(this); window.addEventListener('load', _resetTimer, true); @@ -114,6 +123,10 @@ class IdleManager { * @param {IdleCB} callback function to be called when user goes idle */ public registerCallback(callback: IdleCB): void { + if (this._usingDefaultCb) { + this.callbacks = []; + } + this._usingDefaultCb = false; this.callbacks.push(callback); } @@ -121,7 +134,6 @@ class IdleManager { * Cleans up the idle manager and its listeners */ public exit(): void { - this.callbacks.forEach(cb => cb()); clearTimeout(this.timeoutID); window.removeEventListener('load', this._resetTimer, true); @@ -129,6 +141,7 @@ class IdleManager { events.forEach(function (name) { document.removeEventListener(name, _resetTimer, true); }); + this.callbacks.forEach(cb => cb()); } /** diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index b6ca3b4cc..0d88abb7f 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -52,6 +52,8 @@ export interface IdleOptions extends IdleManagerOptions { disableIdle?: boolean; } +export * from './idleManager'; + export interface AuthClientLoginOptions { /** * Identity provider From 5defc6154512c9d66fc9ba54c0073c41addbeafa Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 5 May 2022 14:27:15 -0700 Subject: [PATCH 2/7] documentation: updating IdleManager readme and changelog --- docs/generated/changelog.html | 7 +++++++ packages/auth-client/README.md | 35 ++++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html index 9d2ad0c19..e42febf40 100644 --- a/docs/generated/changelog.html +++ b/docs/generated/changelog.html @@ -10,6 +10,13 @@

Agent-JS Changelog

+

Version 0.11.2

+
    +
  • + Adds a default callback to the IdleManager that will refresh the page after clearing the + storage +
  • +

Version 0.11.1

  • Fix for a corner case that could lead to incorrect decoding of record types.
  • diff --git a/packages/auth-client/README.md b/packages/auth-client/README.md index 6380ed484..f8560d11b 100644 --- a/packages/auth-client/README.md +++ b/packages/auth-client/README.md @@ -56,16 +56,39 @@ const actor = Actor.createActor(idlFactory, { }); ``` -

    Idle Update

    +

    Idle Management

    -As of 0.10.5, the authClient has two notable new features: +The AuthClient provides two forms of security for session management. The first is built into the Internet Identity delegation - the `maxTimeToLive` option in nanoseconds determines how long the `DelegationIdentity` you get back will be valid for. The second is the Idle Manager, which moniters keyboard, mouse and touchscreen identity. The Idle Manager will automatically log you out if you don't interact with the browser for a period of time. -1. the maxTimeToLive is now a set to 8 hours by default, down from 24. -2. you can now set a timeout for when your identity will be considered idle +If you pass no options to the IdleManager, it will log you out after 10 minutes of inactivity by removing the `DelegationIdentity` from localStorage and then calling `window.location.reload()`. -These defaults are more conservative, out of the interest of protecting users as more sites are starting to manage ICP and NFT's. You can override these defaults, and opt out of the Idle Manager if you so choose. For more details, see the [forum discussion](https://forum.dfinity.org/t/authclient-update-idle-timeouts). +If you pass an `onIdle` option, it will call that function when the user is idle, replacing the default window.location.reload() behavior. You can also register callbacks after the idleManager is created with the `idleManager.registerCallback()` method, which will also replace the default callback. -Additionally, we now support utility methods in Agents to invalidate an identity. It is suggested that you use this method to invalidate an identity once the user goes idle by calling `Actor.getAgent(actor).invalidateIdentity()`. See the below code for an example: +The full set of options for the IdleManager is: + +```js + /** + * Callback after the user has gone idle + */ + onIdle?: IdleCB; + /** + * timeout in ms + * @default 30 minutes [600_000] + */ + idleTimeout?: number; + /** + * capture scroll events + * @default false + */ + captureScroll?: boolean; + /** + * scroll debounce time in ms + * @default 100 + */ + scrollDebounce?: number; +``` + +### IdleManager Example Usage ```js const authClient = await AuthClient.create({ From 60ffe664135ef57d5df6be64fab36660526ba4d8 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 5 May 2022 15:22:44 -0700 Subject: [PATCH 3/7] updating exports, fixing test typescript errors --- packages/auth-client/src/idleManager.test.ts | 6 +++--- packages/auth-client/src/idleManager.ts | 7 +++---- packages/auth-client/src/index.ts | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/auth-client/src/idleManager.test.ts b/packages/auth-client/src/idleManager.test.ts index 8ad844c6c..7e4b519a4 100644 --- a/packages/auth-client/src/idleManager.test.ts +++ b/packages/auth-client/src/idleManager.test.ts @@ -1,12 +1,12 @@ -import IdleManager from './idleManager'; +import { IdleManager } from './idleManager'; jest.useFakeTimers(); const { location } = window; beforeEach(() => { - delete window.location; - window.location = { reload: jest.fn() }; + delete (window as any).location; + (window as any).location = { reload: jest.fn() }; }); describe('IdleManager tests', () => { diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index e14a004cc..70713c10b 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -26,9 +26,10 @@ const events = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'wheel']; /** * Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle` and registered callbacks. - * @see {@link IdleManager} + * By default, the IdleManager will log a user out after 30 minutes of inactivity, and reload the page. + * To override these defaults, you can pass an `onIdle` callback, or configure a custom `idleTimeout` in milliseconds */ -class IdleManager { +export class IdleManager { callbacks: IdleCB[] = []; idleTimeout: IdleManagerOptions['idleTimeout'] = 30 * 60 * 1000; timeoutID?: number = undefined; @@ -153,5 +154,3 @@ class IdleManager { this.timeoutID = window.setTimeout(exit, this.idleTimeout); } } - -export default IdleManager; diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 0d88abb7f..62c55f2b7 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -14,7 +14,7 @@ import { Ed25519KeyIdentity, } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; -import IdleManager, { IdleManagerOptions } from './idleManager'; +import { IdleManager, IdleManagerOptions } from './idleManager'; const KEY_LOCALSTORAGE_KEY = 'identity'; const KEY_LOCALSTORAGE_DELEGATION = 'delegation'; From ebcb7c8ebf1a3cb9d58184ccb299599c76b28ed9 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Fri, 6 May 2022 15:32:56 -0700 Subject: [PATCH 4/7] refactor wip --- packages/auth-client/src/idleManager.test.ts | 12 ++----- packages/auth-client/src/idleManager.ts | 18 ++-------- packages/auth-client/src/index.test.ts | 35 ++++++++++++++++++++ packages/auth-client/src/index.ts | 24 ++++++++++++-- 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/auth-client/src/idleManager.test.ts b/packages/auth-client/src/idleManager.test.ts index 7e4b519a4..3961f3add 100644 --- a/packages/auth-client/src/idleManager.test.ts +++ b/packages/auth-client/src/idleManager.test.ts @@ -19,21 +19,15 @@ describe('IdleManager tests', () => { expect(cb).toHaveBeenCalled(); manager.exit(); }); - it('should automatically reload the page if no other idle callback is registered', () => { - IdleManager.create(); - - expect(window.location.reload).not.toHaveBeenCalled(); - // simulate user being inactive for 10 minutes - jest.advanceTimersByTime(10 * 60 * 1000); - expect(window.location.reload).toHaveBeenCalled(); - }); it('should replace the default callback if a callback is passed during creation', () => { - IdleManager.create({ onIdle: jest.fn() }); + const idleFn = jest.fn(); + IdleManager.create({ onIdle: idleFn }); expect(window.location.reload).not.toHaveBeenCalled(); // simulate user being inactive for 10 minutes jest.advanceTimersByTime(10 * 60 * 1000); expect(window.location.reload).not.toHaveBeenCalled(); + expect(idleFn).toBeCalled(); }); it('should replace the default callback if a callback is registered', () => { const manager = IdleManager.create(); diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index 70713c10b..604aada1e 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -26,14 +26,13 @@ const events = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'wheel']; /** * Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle` and registered callbacks. - * By default, the IdleManager will log a user out after 30 minutes of inactivity, and reload the page. + * By default, the IdleManager will log a user out after 10 minutes of inactivity. * To override these defaults, you can pass an `onIdle` callback, or configure a custom `idleTimeout` in milliseconds */ export class IdleManager { callbacks: IdleCB[] = []; - idleTimeout: IdleManagerOptions['idleTimeout'] = 30 * 60 * 1000; + idleTimeout: IdleManagerOptions['idleTimeout'] = 10 * 60 * 1000; timeoutID?: number = undefined; - private _usingDefaultCb = false; /** * Creates an {@link IdleManager} @@ -78,16 +77,9 @@ export class IdleManager { protected constructor(options: IdleManagerOptions = {}) { const { onIdle, idleTimeout = 10 * 60 * 1000 } = options || {}; - const defaultIdleCB = () => { - window.location.reload(); - }; - - this.callbacks = onIdle ? [onIdle] : [defaultIdleCB]; + this.callbacks = onIdle ? [onIdle] : []; this.idleTimeout = idleTimeout; - // If no callback is passed, set flag that we are using the default callback - this._usingDefaultCb = !onIdle; - const _resetTimer = this._resetTimer.bind(this); window.addEventListener('load', _resetTimer, true); @@ -124,10 +116,6 @@ export class IdleManager { * @param {IdleCB} callback function to be called when user goes idle */ public registerCallback(callback: IdleCB): void { - if (this._usingDefaultCb) { - this.callbacks = []; - } - this._usingDefaultCb = false; this.callbacks.push(callback); } diff --git a/packages/auth-client/src/index.test.ts b/packages/auth-client/src/index.test.ts index 668bc0f05..7100e4cc8 100644 --- a/packages/auth-client/src/index.test.ts +++ b/packages/auth-client/src/index.test.ts @@ -31,6 +31,13 @@ class IdpMock { } } +const { location, fetch } = window; + +afterEach(() => { + delete (window as any).location; + (window as any).location = location; +}); + describe('Auth Client', () => { it('should initialize with an AnonymousIdentity', async () => { const test = await AuthClient.create(); @@ -97,6 +104,34 @@ describe('Auth Client', () => { expect((error as AgentError).message).toBe(expectedError); } }); + it('should log out after idle and reload the window by default', async () => { + delete (window as any).location; + (window as any).location = { reload: jest.fn(), fetch }; + const mockFetch: jest.Mock = jest.fn(); + + const canisterId = Principal.fromText('2chl6-4hpzw-vqaaa-aaaaa-c'); + const actorInterface = () => { + return IDL.Service({ + greet: IDL.Func([IDL.Text], [IDL.Text]), + }); + }; + + // setup auth client + const test = await AuthClient.create({ + identity: await Ed25519KeyIdentity.generate(), + idleOptions: { + idleTimeout: 1000, + }, + }); + const httpAgent = new HttpAgent({ fetch: mockFetch, host: 'http://127.0.0.1:8000' }); + const actor = Actor.createActor(actorInterface, { canisterId, agent: httpAgent }); + + // simulate user being inactive for 10 minutes + jest.advanceTimersByTime(10 * 60 * 1000); + + expect(window.location.reload).toBeCalled(); + }); + /** * This test reflects a feature that may be added at a future date, * allowing the authClient to register actors for automatic invalidation diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 62c55f2b7..46c0bdbc6 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -50,6 +50,12 @@ export interface IdleOptions extends IdleManagerOptions { * @default false */ disableIdle?: boolean; + + /** + * Disables default idle behavior - call logout & reload window + * @default false + */ + disableDefaultIdleCallback?: boolean; } export * from './idleManager'; @@ -195,6 +201,7 @@ export class AuthClient { * @see {@link AuthClientStorage} * @param {IdleOptions} options.idleOptions Configures an {@link IdleManager} * @see {@link IdleOptions} + * Default behavior is to clear stored identity and reload the page when a user goes idle, unless you set the disableDefaultIdleCallback flag or pass in a custom idle callback. * @example * const authClient = await AuthClient.create({ * idleOptions: { @@ -270,7 +277,7 @@ export class AuthClient { ? undefined : IdleManager.create(options.idleOptions); - return new this(identity, key, chain, storage, idleManager); + return new this(identity, key, chain, storage, idleManager, options); } protected constructor( @@ -279,13 +286,26 @@ export class AuthClient { private _chain: DelegationChain | null, private _storage: AuthClientStorage, public readonly idleManager: IdleManager | undefined, + private _createOptions?: AuthClientCreateOptions, // A handle on the IdP window. private _idpWindow?: Window, // The event handler for processing events from the IdP. private _eventHandler?: (event: MessageEvent) => void, ) { const logout = this.logout.bind(this); - this.idleManager?.registerCallback(logout); + /** + * Default behavior is to clear stored identity and reload the page. + * By either setting the disableDefaultIdleCallback flag or passing in a custom idle callback, we will ignore this config + */ + this.idleManager?.registerCallback(() => { + if ( + !_createOptions?.idleOptions?.onIdle && + !_createOptions?.idleOptions?.disableDefaultIdleCallback + ) { + logout(); + location.reload(); + } + }); } private _handleSuccess(message: InternetIdentityAuthResponseSuccess, onSuccess?: () => void) { From 95e54d579dc66fcd15fdc74c42cc14437608af3e Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 11 May 2022 11:36:33 -0700 Subject: [PATCH 5/7] tests for overriding default cb --- packages/auth-client/src/idleManager.ts | 1 + packages/auth-client/src/index.test.ts | 36 ++++++++++++++++++++++++- packages/auth-client/src/index.ts | 15 +++++------ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index 604aada1e..459eab129 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -130,6 +130,7 @@ export class IdleManager { events.forEach(function (name) { document.removeEventListener(name, _resetTimer, true); }); + this.callbacks; //? this.callbacks.forEach(cb => cb()); } diff --git a/packages/auth-client/src/index.test.ts b/packages/auth-client/src/index.test.ts index 7100e4cc8..b8b91517d 100644 --- a/packages/auth-client/src/index.test.ts +++ b/packages/auth-client/src/index.test.ts @@ -33,6 +33,9 @@ class IdpMock { const { location, fetch } = window; +beforeEach(() => { + jest.useFakeTimers(); +}); afterEach(() => { delete (window as any).location; (window as any).location = location; @@ -65,7 +68,6 @@ describe('Auth Client', () => { expect(test.idleManager).toBeDefined(); }); it('should be able to invalidate an identity after going idle', async () => { - jest.useFakeTimers(); // setup actor const identity = Ed25519KeyIdentity.generate(); const mockFetch: jest.Mock = jest.fn(); @@ -131,6 +133,38 @@ describe('Auth Client', () => { expect(window.location.reload).toBeCalled(); }); + it('should not reload the page if the default callback is disabled', async () => { + delete (window as any).location; + (window as any).location = { reload: jest.fn(), fetch }; + const test = await AuthClient.create({ + idleOptions: { + idleTimeout: 1000, + disableDefaultIdleCallback: true, + }, + }); + + // simulate user being inactive for 10 minutes + jest.advanceTimersByTime(10 * 60 * 1000); + + expect(window.location.reload).not.toBeCalled(); + }); + it('should not reload the page if a callback is provided', async () => { + delete (window as any).location; + (window as any).location = { reload: jest.fn(), fetch }; + const idleCb = jest.fn(); + const test = await AuthClient.create({ + idleOptions: { + idleTimeout: 1000, + onIdle: idleCb, + }, + }); + + // simulate user being inactive for 10 minutes + jest.advanceTimersByTime(10 * 60 * 1000); + + expect(window.location.reload).not.toBeCalled(); + expect(idleCb).toBeCalled(); + }); /** * This test reflects a feature that may be added at a future date, diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 46c0bdbc6..f226e247d 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -272,7 +272,6 @@ export class AuthClient { key = null; } } - const idleManager = options.idleOptions?.disableIdle ? undefined : IdleManager.create(options.idleOptions); @@ -286,26 +285,24 @@ export class AuthClient { private _chain: DelegationChain | null, private _storage: AuthClientStorage, public readonly idleManager: IdleManager | undefined, - private _createOptions?: AuthClientCreateOptions, + private _createOptions: AuthClientCreateOptions | undefined, // A handle on the IdP window. private _idpWindow?: Window, // The event handler for processing events from the IdP. private _eventHandler?: (event: MessageEvent) => void, ) { const logout = this.logout.bind(this); + const idleOptions = _createOptions?.idleOptions; /** * Default behavior is to clear stored identity and reload the page. * By either setting the disableDefaultIdleCallback flag or passing in a custom idle callback, we will ignore this config */ - this.idleManager?.registerCallback(() => { - if ( - !_createOptions?.idleOptions?.onIdle && - !_createOptions?.idleOptions?.disableDefaultIdleCallback - ) { + if (!idleOptions?.onIdle && !idleOptions?.disableDefaultIdleCallback) { + this.idleManager?.registerCallback(() => { logout(); location.reload(); - } - }); + }); + } } private _handleSuccess(message: InternetIdentityAuthResponseSuccess, onSuccess?: () => void) { From 308c0e230d5d98c62d58d933ad058905cbdde34a Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 11 May 2022 12:36:25 -0700 Subject: [PATCH 6/7] updating documentation --- packages/auth-client/README.md | 17 +++++++++++++++++ packages/auth-client/src/idleManager.ts | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/auth-client/README.md b/packages/auth-client/README.md index f8560d11b..ba92c9652 100644 --- a/packages/auth-client/README.md +++ b/packages/auth-client/README.md @@ -88,12 +88,29 @@ The full set of options for the IdleManager is: scrollDebounce?: number; ``` +Additionally, the AuthClient accepts a couple additional flags to `idleOptions` to control the IdleManager: + +```js + /** + * Disables idle functionality for {@link IdleManager} + * @default false + */ + disableIdle?: boolean; + + /** + * Disables default idle behavior - call logout & reload window + * @default false + */ + disableDefaultIdleCallback?: boolean; +``` + ### IdleManager Example Usage ```js const authClient = await AuthClient.create({ idleOptions: { idleTimeout: 1000 * 60 * 30, // set to 30 minutes + disableDefaultIdleCallback: true // disable the default reload behavior } }); // ...authClient.login() diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index 459eab129..604aada1e 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -130,7 +130,6 @@ export class IdleManager { events.forEach(function (name) { document.removeEventListener(name, _resetTimer, true); }); - this.callbacks; //? this.callbacks.forEach(cb => cb()); } From c16c4dfed9acd78dfa74903fadd197759d5bf49c Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 11 May 2022 13:35:09 -0700 Subject: [PATCH 7/7] storage tests --- packages/auth-client/src/index.test.ts | 73 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/auth-client/src/index.test.ts b/packages/auth-client/src/index.test.ts index b8b91517d..e3d9276c5 100644 --- a/packages/auth-client/src/index.test.ts +++ b/packages/auth-client/src/index.test.ts @@ -3,7 +3,7 @@ import { AgentError } from '@dfinity/agent/lib/cjs/errors'; import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; -import { AuthClient, ERROR_USER_INTERRUPT } from './index'; +import { AuthClient, AuthClientStorage, ERROR_USER_INTERRUPT } from './index'; /** * A class for mocking the IDP service. @@ -107,6 +107,24 @@ describe('Auth Client', () => { } }); it('should log out after idle and reload the window by default', async () => { + setup({ + onAuthRequest: () => { + // Send a valid request. + idpMock.send({ + kind: 'authorize-client-success', + delegations: [ + { + delegation: { + pubkey: Uint8Array.from([]), + expiration: BigInt(0), + }, + signature: Uint8Array.from([]), + }, + ], + userPublicKey: Uint8Array.from([]), + }); + }, + }); delete (window as any).location; (window as any).location = { reload: jest.fn(), fetch }; const mockFetch: jest.Mock = jest.fn(); @@ -118,34 +136,85 @@ describe('Auth Client', () => { }); }; + const storage: AuthClientStorage = { + remove: jest.fn(), + get: jest.fn(), + set: jest.fn(), + }; + // setup auth client const test = await AuthClient.create({ - identity: await Ed25519KeyIdentity.generate(), + storage, idleOptions: { idleTimeout: 1000, }, }); + + // Test login flow + await test.login({ identityProvider: 'http://localhost' }); + + expect(storage.set).toBeCalled(); + expect(storage.remove).not.toBeCalled(); + const httpAgent = new HttpAgent({ fetch: mockFetch, host: 'http://127.0.0.1:8000' }); const actor = Actor.createActor(actorInterface, { canisterId, agent: httpAgent }); // simulate user being inactive for 10 minutes jest.advanceTimersByTime(10 * 60 * 1000); + // Storage should be cleared by default after logging out + expect(storage.remove).toBeCalled(); + expect(window.location.reload).toBeCalled(); }); it('should not reload the page if the default callback is disabled', async () => { + setup({ + onAuthRequest: () => { + // Send a valid request. + idpMock.send({ + kind: 'authorize-client-success', + delegations: [ + { + delegation: { + pubkey: Uint8Array.from([]), + expiration: BigInt(0), + }, + signature: Uint8Array.from([]), + }, + ], + userPublicKey: Uint8Array.from([]), + }); + }, + }); delete (window as any).location; (window as any).location = { reload: jest.fn(), fetch }; + + const storage: AuthClientStorage = { + remove: jest.fn(), + get: jest.fn(), + set: jest.fn(), + }; + const test = await AuthClient.create({ + storage, idleOptions: { idleTimeout: 1000, disableDefaultIdleCallback: true, }, }); + // Test login flow + await test.login({ identityProvider: 'http://localhost' }); + + expect(storage.set).toBeCalled(); + expect(storage.remove).not.toBeCalled(); + // simulate user being inactive for 10 minutes jest.advanceTimersByTime(10 * 60 * 1000); + // Storage should not be cleared + expect(storage.remove).not.toBeCalled(); + // Page should not be reloaded expect(window.location.reload).not.toBeCalled(); }); it('should not reload the page if a callback is provided', async () => {