From bda00a0368508401ea97effa1f384af83e92cfbb Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 30 Mar 2022 16:19:22 -0700 Subject: [PATCH 1/5] Sets default timeout to 8 hours also updates Readme and JSDOC --- packages/auth-client/README.md | 14 +++--- packages/auth-client/src/index.ts | 84 ++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/packages/auth-client/README.md b/packages/auth-client/README.md index 8c28892d0..00da57bc7 100644 --- a/packages/auth-client/README.md +++ b/packages/auth-client/README.md @@ -59,8 +59,7 @@ As of 0.10.5, you can now set a timeout for when your identity will be considere ```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() @@ -72,14 +71,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.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. diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 30ff2efec..32570dfdb 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -54,11 +54,13 @@ export interface IdleOptions extends IdleManagerOptions { 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; /** @@ -171,7 +173,36 @@ type IdentityServiceResponseMessage = AuthReadyMessage | AuthResponse; type AuthResponse = AuthResponseSuccess | AuthResponseFailure; export class AuthClient { - public static async create(options: AuthClientCreateOptions = {}): Promise { + /** + * Create an AuthClient to manage authentication and identity + * @constructs {@link AuthClient} + * @param {AuthClientCreateOptions} options {@link AuthClientCreateOptions} + * @param options.identity {@link SignIdentity} + * @description Optional Identity to use as the base + * @param options.storage {@link AuthClientStorage} + * @description Storage mechanism for delegration credentials + * @param {IdleOptions} options.idleOptions {@link IdleOptions} + * @description Configures an {@link IdleManager} + */ + 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 { const storage = options.storage ?? new LocalStorage('ic-'); let key: null | SignIdentity = null; @@ -278,7 +309,47 @@ export class AuthClient { return !this.getIdentity().getPrincipal().isAnonymous() && this._chain !== null; } - public async login(options?: AuthClientLoginOptions): Promise { + /** + * 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://.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); + /** + * Callback in case authentication fails + */ + onError?: ((error?: string) => void) | ((error?: string) => Promise); + }): Promise { let key = this._key; if (!key) { // Create a new key (whether or not one was in storage). @@ -287,9 +358,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( @@ -305,7 +375,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); From 7330e5819c7b8e9cd6bdeb6815c5c607418defad Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 30 Mar 2022 16:23:30 -0700 Subject: [PATCH 2/5] example for create --- packages/auth-client/src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 32570dfdb..837fc7b7b 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -46,7 +46,7 @@ export interface AuthClientCreateOptions { export interface IdleOptions extends IdleManagerOptions { /** - * Disables idle functionality + * Disables idle functionality for {@link IdleManager} * @default false */ disableIdle?: boolean; @@ -183,6 +183,12 @@ export class AuthClient { * @description Storage mechanism for delegration credentials * @param {IdleOptions} options.idleOptions {@link IdleOptions} * @description Configures an {@link IdleManager} + * @example + * const authClient = await AuthClient.create({ + * idleOptions: { + * disableIdle: true + * } + * }) */ public static async create( options: { From 93e6a63e67276fc5e91cff6d8d4d8c162fb462a0 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 30 Mar 2022 16:57:38 -0700 Subject: [PATCH 3/5] additional documentation --- packages/auth-client/README.md | 19 ++++++++--- packages/auth-client/src/idleManager.ts | 42 +++++++++++++++---------- packages/auth-client/src/index.ts | 20 +++++++----- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/packages/auth-client/README.md b/packages/auth-client/README.md index 00da57bc7..7782c7606 100644 --- a/packages/auth-client/README.md +++ b/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. @@ -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); }, }); ``` @@ -52,9 +56,16 @@ const actor = Actor.createActor(idlFactory, { }); ``` -## Idle Timeout +

Idle Update

+ +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 + +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). -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. +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({ @@ -76,7 +87,7 @@ refreshLogin() { authClient.login({ onSuccess: async () => { const newIdentity = await AuthClient.getIdentity(); - actor.replaceIdentity(newIdentity); + Actor.getAgent(actor).replaceIdentity(newIdentity); } }); } diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index fc32d9a66..b323e0159 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -1,3 +1,4 @@ +/** @module IdleManager */ type IdleCB = () => unknown; export type IdleManagerOptions = { /** @@ -24,9 +25,8 @@ export type IdleManagerOptions = { const events = ['mousedown', 'mousemove', 'keypress', '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[] = []; @@ -34,9 +34,10 @@ class IdleManager { 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 @@ -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] @@ -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 @@ -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); @@ -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); diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 837fc7b7b..c9c794032 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -1,3 +1,4 @@ +/** @module AuthClient */ import { Actor, AnonymousIdentity, @@ -172,17 +173,22 @@ interface AuthResponseFailure { type IdentityServiceResponseMessage = AuthReadyMessage | AuthResponse; type AuthResponse = AuthResponseSuccess | AuthResponseFailure; +/** + * Tool to manage authentication and identity + * @see {@link AuthClient} + */ export class AuthClient { /** * Create an AuthClient to manage authentication and identity * @constructs {@link AuthClient} - * @param {AuthClientCreateOptions} options {@link AuthClientCreateOptions} - * @param options.identity {@link SignIdentity} - * @description Optional Identity to use as the base - * @param options.storage {@link AuthClientStorage} - * @description Storage mechanism for delegration credentials - * @param {IdleOptions} options.idleOptions {@link IdleOptions} - * @description Configures an {@link IdleManager} + * @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: { From 6943323989b426056a245d154f00aa1b7fcc74dd Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 30 Mar 2022 17:01:44 -0700 Subject: [PATCH 4/5] changelog --- docs/generated/changelog.html | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html index 6b3ead8fd..138919cb3 100644 --- a/docs/generated/changelog.html +++ b/docs/generated/changelog.html @@ -22,6 +22,7 @@

Version 0.10.5

of logging the user out and prompting re-authentication. See the @dfinity/auth-client Readme for more details +
  • Reduces the maxTimeToLive default setting from 24 hours to 8
  • Version 0.10.3

      From 989cd160aa645c9d8e4b0557fd492927fc1f1566 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 31 Mar 2022 08:31:42 -0700 Subject: [PATCH 5/5] keydown instead of deprecated keypress --- packages/auth-client/src/idleManager.test.ts | 2 +- packages/auth-client/src/idleManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/auth-client/src/idleManager.test.ts b/packages/auth-client/src/idleManager.test.ts index 3b7ecb986..fa1d55a9c 100644 --- a/packages/auth-client/src/idleManager.test.ts +++ b/packages/auth-client/src/idleManager.test.ts @@ -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); diff --git a/packages/auth-client/src/idleManager.ts b/packages/auth-client/src/idleManager.ts index b323e0159..08f71a7b4 100644 --- a/packages/auth-client/src/idleManager.ts +++ b/packages/auth-client/src/idleManager.ts @@ -22,7 +22,7 @@ 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` and registered callbacks.