Skip to content

Commit

Permalink
chore: reduces default delegation expiration to 8 hours (#550)
Browse files Browse the repository at this point in the history
* Sets default timeout to 8 hours
also updates Readme and JSDOC

* example for create

* additional documentation

* changelog

* keydown instead of deprecated keypress
  • Loading branch information
krpeacock committed Apr 1, 2022
1 parent 178cb01 commit d1b5e24
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 36 deletions.
1 change: 1 addition & 0 deletions docs/generated/changelog.html
Expand Up @@ -22,6 +22,7 @@ <h2>Version 0.10.5</h2>
of logging the user out and prompting re-authentication. See the @dfinity/auth-client
Readme for more details
</li>
<li>Reduces the maxTimeToLive default setting from 24 hours to 8</li>
</ul>
<h2>Version 0.10.3</h2>
<ul>
Expand Down
31 changes: 22 additions & 9 deletions packages/auth-client/README.md
@@ -1,5 +1,7 @@
# @dfinity/auth-client

> 0.10.5 Idle update - see changes [here](#0.10.5-idle-update)
Simple interface to get your web application authenticated with the Internet Identity Service

Visit the [Dfinity Forum](https://forum.dfinity.org/) and [SDK Documentation](https://sdk.dfinity.org/docs/index.html) for more information and support building on the Internet Computer.
Expand Down Expand Up @@ -32,8 +34,10 @@ The authClient can log in with

```js
authClient.login({
// 7 days in nanoseconds
maxTimeToLive: BigInt(7 * 24 * 60 * 60 * 1000 * 1000 * 1000),
onSuccess: async () => {
// authClient now has an identity
handleAuthenticated(authClient);
},
});
```
Expand All @@ -52,15 +56,21 @@ const actor = Actor.createActor(idlFactory, {
});
```

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

As of 0.10.5, the authClient has two notable new features:

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

As of 0.10.5, you can now set a timeout for when your identity will be considered idle, and you can use that to log out or prompt your user to refresh their authentication. This is recommended for applications managing tokens or other valuable information.
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).

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:

```js
const authClient = await AuthClient.create({
idleOptions: {
idleTimeout: 1000 * 60 * 30, // default is 30 minutes
disableIdle: false, // set to true to disable idle timeout
idleTimeout: 1000 * 60 * 30, // set to 30 minutes
}
});
// ...authClient.login()
Expand All @@ -72,14 +82,17 @@ const actor = Actor.createActor(idlFactory, {
canisterId,
});

authClient.registerActor("ii", actor);

refreshLogin() {
// prompt the user then refresh their authentication
authClient.login();
authClient.login({
onSuccess: async () => {
const newIdentity = await AuthClient.getIdentity();
Actor.getAgent(actor).replaceIdentity(newIdentity);
}
});
}

authClient.idleManager?.registerCallback?.(refreshLogin)
authClient.idleManager?.registerCallback?.(refreshLogin);
```
In this code, we create an `authClient` with an idle timeout of 30 minutes. When the user is idle, we invalidate their identity and prompt them to login again.
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-client/src/idleManager.test.ts
Expand Up @@ -30,7 +30,7 @@ describe('IdleManager tests', () => {
// simulate user being inactive for 9 minutes
jest.advanceTimersByTime(9 * 60 * 1000);
expect(cb).not.toHaveBeenCalled();
document.dispatchEvent(new KeyboardEvent('keypress'));
document.dispatchEvent(new KeyboardEvent('keydown'));

// wait 5 minutes
jest.advanceTimersByTime(5 * 60 * 1000);
Expand Down
44 changes: 26 additions & 18 deletions packages/auth-client/src/idleManager.ts
@@ -1,3 +1,4 @@
/** @module IdleManager */
type IdleCB = () => unknown;
export type IdleManagerOptions = {
/**
Expand All @@ -21,22 +22,22 @@ export type IdleManagerOptions = {
scrollDebounce?: number;
};

const events = ['mousedown', 'mousemove', 'keypress', 'touchstart', 'wheel'];
const events = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'wheel'];

/**
* Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle`.
*
* @param {IdleManagerOptions} options
* Detects if the user has been idle for a duration of `idleTimeout` ms, and calls `onIdle` and registered callbacks.
* @see {@link IdleManager}
*/
class IdleManager {
callbacks: IdleCB[] = [];
idleTimeout: IdleManagerOptions['idleTimeout'] = 30 * 60 * 1000;
timeoutID?: number = undefined;

/**
* Creates an idle manager
* Creates an {@link IdleManager}
* @param {IdleManagerOptions} options Optional configuration
* @param options.onIdle Callback once user has been idle. Use to prompt for fresh login, and use Actor.agentOf(your_actor).invalidateIdentity() to protect the user
* @see {@link IdleManagerOptions}
* @param options.onIdle Callback once user has been idle. Use to prompt for fresh login, and use `Actor.agentOf(your_actor).invalidateIdentity()` to protect the user
* @param options.idleTimeout timeout in ms
* @param options.captureScroll capture scroll events
* @param options.scrollDebounce scroll debounce time in ms
Expand All @@ -45,8 +46,9 @@ class IdleManager {
options: {
/**
* Callback after the user has gone idle
* @see {@link IdleCB}
*/
onIdle?: IdleCB;
onIdle?: () => unknown;
/**
* timeout in ms
* @default 10 minutes [600_000]
Expand All @@ -67,17 +69,21 @@ class IdleManager {
return new this(options);
}

/**
* @protected
* @param options {@link IdleManagerOptions}
*/
protected constructor(options: IdleManagerOptions = {}) {
const { onIdle, idleTimeout = 10 * 60 * 1000 } = options || {};
this.callbacks = onIdle ? [onIdle] : [];
this.idleTimeout = idleTimeout;

const resetTimer = this.resetTimer.bind(this);
const _resetTimer = this._resetTimer.bind(this);

window.addEventListener('load', resetTimer, true);
window.addEventListener('load', _resetTimer, true);

events.forEach(function (name) {
document.addEventListener(name, resetTimer, true);
document.addEventListener(name, _resetTimer, true);
});

// eslint-disable-next-line @typescript-eslint/ban-types
Expand All @@ -97,16 +103,15 @@ class IdleManager {

if (options?.captureScroll) {
// debounce scroll events
const scroll = debounce(resetTimer, options?.scrollDebounce ?? 100);
const scroll = debounce(_resetTimer, options?.scrollDebounce ?? 100);
window.addEventListener('scroll', scroll, true);
}

resetTimer();
_resetTimer();
}

/**
* adds a callback to the list of callbacks
* @param {IdleCB} callback
* @param {IdleCB} callback function to be called when user goes idle
*/
public registerCallback(callback: IdleCB): void {
this.callbacks.push(callback);
Expand All @@ -118,15 +123,18 @@ class IdleManager {
public exit(): void {
this.callbacks.forEach(cb => cb());
clearTimeout(this.timeoutID);
window.removeEventListener('load', this.resetTimer, true);
window.removeEventListener('load', this._resetTimer, true);

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

resetTimer(): void {
/**
* Resets the timeouts during cleanup
*/
_resetTimer(): void {
const exit = this.exit.bind(this);
window.clearTimeout(this.timeoutID);
this.timeoutID = window.setTimeout(exit, this.idleTimeout);
Expand Down
98 changes: 90 additions & 8 deletions packages/auth-client/src/index.ts
@@ -1,3 +1,4 @@
/** @module AuthClient */
import {
Actor,
AnonymousIdentity,
Expand Down Expand Up @@ -46,19 +47,21 @@ export interface AuthClientCreateOptions {

export interface IdleOptions extends IdleManagerOptions {
/**
* Disables idle functionality
* Disables idle functionality for {@link IdleManager}
* @default false
*/
disableIdle?: boolean;
}

export interface AuthClientLoginOptions {
/**
* Identity provider. By default, use the identity service.
* Identity provider
* @default "https://identity.ic0.app"
*/
identityProvider?: string | URL;
/**
* Expiration of the authentication in nanoseconds
* @default BigInt(8) hours * BigInt(3_600_000_000_000) nanoseconds
*/
maxTimeToLive?: bigint;
/**
Expand Down Expand Up @@ -170,8 +173,48 @@ interface AuthResponseFailure {
type IdentityServiceResponseMessage = AuthReadyMessage | AuthResponse;
type AuthResponse = AuthResponseSuccess | AuthResponseFailure;

/**
* Tool to manage authentication and identity
* @see {@link AuthClient}
*/
export class AuthClient {
public static async create(options: AuthClientCreateOptions = {}): Promise<AuthClient> {
/**
* Create an AuthClient to manage authentication and identity
* @constructs {@link AuthClient}
* @param {AuthClientCreateOptions} options
* @see {@link AuthClientCreateOptions}
* @param options.identity Optional Identity to use as the base
* @see {@link SignIdentity}
* @param options.storage Storage mechanism for delegration credentials
* @see {@link AuthClientStorage}
* @param {IdleOptions} options.idleOptions Configures an {@link IdleManager}
* @see {@link IdleOptions}
* @example
* const authClient = await AuthClient.create({
* idleOptions: {
* disableIdle: true
* }
* })
*/
public static async create(
options: {
/**
* An {@link Identity} to use as the base.
* By default, a new {@link AnonymousIdentity}
*/
identity?: SignIdentity;
/**
* {@link AuthClientStorage}
* @description Optional storage with get, set, and remove. Uses {@link LocalStorage} by default
*/
storage?: AuthClientStorage;
/**
* Options to handle idle timeouts
* @default after 10 minutes, invalidates the identity
*/
idleOptions?: IdleOptions;
} = {},
): Promise<AuthClient> {
const storage = options.storage ?? new LocalStorage('ic-');

let key: null | SignIdentity = null;
Expand Down Expand Up @@ -278,7 +321,47 @@ export class AuthClient {
return !this.getIdentity().getPrincipal().isAnonymous() && this._chain !== null;
}

public async login(options?: AuthClientLoginOptions): Promise<void> {
/**
* AuthClient Login -
* Opens up a new window to authenticate with Internet Identity
* @param {AuthClientLoginOptions} options
* @param options.identityProvider Identity provider
* @param options.maxTimeToLive Expiration of the authentication in nanoseconds
* @param options.onSuccess Callback once login has completed
* @param options.onError Callback in case authentication fails
* @example
* const authClient = await AuthClient.create();
* authClient.login({
* identityProvider: 'http://<canisterID>.localhost:8000',
* maxTimeToLive: BigInt (7) * BigInt(24) * BigInt(3_600_000_000_000) // 1 week
* onSuccess: () => {
* console.log('Login Successful!');
* },
* onError: (error) => {
* console.error('Login Failed: ', error);
* }
* });
*/
public async login(options?: {
/**
* Identity provider
* @default "https://identity.ic0.app"
*/
identityProvider?: string | URL;
/**
* Expiration of the authentication in nanoseconds
* @default BigInt(8) hours * BigInt(3_600_000_000_000) nanoseconds
*/
maxTimeToLive?: bigint;
/**
* Callback once login has completed
*/
onSuccess?: (() => void) | (() => Promise<void>);
/**
* Callback in case authentication fails
*/
onError?: ((error?: string) => void) | ((error?: string) => Promise<void>);
}): Promise<void> {
let key = this._key;
if (!key) {
// Create a new key (whether or not one was in storage).
Expand All @@ -287,9 +370,8 @@ export class AuthClient {
await this._storage.set(KEY_LOCALSTORAGE_KEY, JSON.stringify(key));
}

// Set default maxTimeToLive to 1 day
const defaultTimeToLive =
/* days */ BigInt(1) * /* hours */ BigInt(24) * /* nanoseconds */ BigInt(3600000000000);
// Set default maxTimeToLive to 8 hours
const defaultTimeToLive = /* hours */ BigInt(8) * /* nanoseconds */ BigInt(3_600_000_000_000);

// Create the URL of the IDP. (e.g. https://XXXX/#authorize)
const identityProviderUrl = new URL(
Expand All @@ -305,7 +387,7 @@ export class AuthClient {

// Add an event listener to handle responses.
this._eventHandler = this._getEventHandler(identityProviderUrl, {
maxTimeToLive: defaultTimeToLive,
maxTimeToLive: options?.maxTimeToLive ?? defaultTimeToLive,
...options,
});
window.addEventListener('message', this._eventHandler);
Expand Down

0 comments on commit d1b5e24

Please sign in to comment.