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

feat: idlemanager reload by default #570

Merged
merged 9 commits into from May 18, 2022
7 changes: 7 additions & 0 deletions docs/generated/changelog.html
Expand Up @@ -10,6 +10,13 @@
<h1>Agent-JS Changelog</h1>

<section>
<h2>Version 0.11.2</h2>
<ul>
<li>
Adds a default callback to the IdleManager that will refresh the page after clearing the
storage
</li>
</ul>
<h2>Version 0.11.1</h2>
<ul>
<li>Fix for a corner case that could lead to incorrect decoding of record types.</li>
Expand Down
35 changes: 29 additions & 6 deletions packages/auth-client/README.md
Expand Up @@ -56,16 +56,39 @@ const actor = Actor.createActor(idlFactory, {
});
```

<h2 id="0.10.5-idle-update">Idle Update</h2>
<h2 id="0.10.5-idle-update">Idle Management</h2>

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.
krpeacock marked this conversation as resolved.
Show resolved Hide resolved

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({
Expand Down
35 changes: 34 additions & 1 deletion packages/auth-client/src/idleManager.test.ts
@@ -1,7 +1,14 @@
import IdleManager from './idleManager';
import { IdleManager } from './idleManager';

jest.useFakeTimers();

const { location } = window;

beforeEach(() => {
delete (window as any).location;
(window as any).location = { reload: jest.fn() };
});

describe('IdleManager tests', () => {
it('should call its callback after time spent inactive', () => {
const cb = jest.fn();
Expand All @@ -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();
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
});
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;
Expand Down
24 changes: 18 additions & 6 deletions packages/auth-client/src/idleManager.ts
Expand Up @@ -26,12 +26,14 @@ 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.
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
* 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;
private _usingDefaultCb = false;

/**
* Creates an {@link IdleManager}
Expand Down Expand Up @@ -75,9 +77,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);
Expand Down Expand Up @@ -114,21 +124,25 @@ class IdleManager {
* @param {IdleCB} callback function to be called when user goes idle
*/
public registerCallback(callback: IdleCB): void {
if (this._usingDefaultCb) {
this.callbacks = [];
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
}
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
this._usingDefaultCb = false;
this.callbacks.push(callback);
}

/**
* 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);

const _resetTimer = this._resetTimer.bind(this);
events.forEach(function (name) {
document.removeEventListener(name, _resetTimer, true);
});
this.callbacks.forEach(cb => cb());
}

/**
Expand All @@ -140,5 +154,3 @@ class IdleManager {
this.timeoutID = window.setTimeout(exit, this.idleTimeout);
}
}

export default IdleManager;
4 changes: 3 additions & 1 deletion packages/auth-client/src/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -52,6 +52,8 @@ export interface IdleOptions extends IdleManagerOptions {
disableIdle?: boolean;
}

export * from './idleManager';

export interface AuthClientLoginOptions {
/**
* Identity provider
Expand Down