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

chore: reduces default delegation expiration to 8 hours #550

Merged
merged 5 commits into from Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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