Skip to content

Commit

Permalink
Auth Strategies (#1257)
Browse files Browse the repository at this point in the history
* auth strategies

* default to no auth

* rename base auth strategy

* rename base strategy cont.

* refactor auth strategy methods and LocalAuth

* activate old session options even if is falsy value

* move restartOnAuthFail to LegacyAuthStrategy option

* add link to guide item

* update example/shell

* types
  • Loading branch information
pedroslopez committed Feb 28, 2022
1 parent 0d55d40 commit f6de161
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 116 deletions.
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,4 @@ typings/

# Test sessions
*session.json

# user data
WWebJS/
userDataDir/
.wwebjs_auth/
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ yarn-debug.log*
yarn-error.log*

*session.json
.wwebjs_auth/

.env
tools/
tests/
13 changes: 3 additions & 10 deletions example.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
const { Client, Location, List, Buttons } = require('./index');
const { Client, Location, List, Buttons, LocalAuth } = require('./index');

const client = new Client({
clientId: 'example',
const client = new Client({
authStrategy: new LocalAuth(),
puppeteer: { headless: false }
});

// You also could connect to an existing instance of a browser
// {
// puppeteer: {
// browserWSEndpoint: `ws://localhost:3000`
// }
// }

client.initialize();

client.on('qr', (qr) => {
Expand Down
78 changes: 52 additions & 26 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ declare namespace WAWebJS {
/** Emitted when authentication is successful */
on(event: 'authenticated', listener: (
/**
* Object containing session information. Can be used to restore the session
* @deprecated
* Object containing session information, when using LegacySessionAuth. Can be used to restore the session
*/
session?: ClientSession
) => void): this
Expand Down Expand Up @@ -297,35 +296,23 @@ declare namespace WAWebJS {
/** Options for initializing the whatsapp client */
export interface ClientOptions {
/** Timeout for authentication selector in puppeteer
* @default 45000 */
* @default 0 */
authTimeoutMs?: number,
/** Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/ */
puppeteer?: puppeteer.LaunchOptions & puppeteer.BrowserLaunchArgumentOptions & puppeteer.BrowserConnectOptions
/** Refresh interval for qr code (how much time to wait before checking if the qr code has changed)
* @default 20000 */
qrRefreshIntervalMs?: number
/** Timeout for qr code selector in puppeteer
* @default 45000 */
qrTimeoutMs?: number,
/** How many times should the qrcode be refreshed before giving up
/** Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used. */
authStrategy?: AuthStrategy,
/** How many times should the qrcode be refreshed before giving up
* @default 0 (disabled) */
qrMaxRetries?: number,
/** Restart client with a new session (i.e. use null 'session' var) if authentication fails
* @default false */
restartOnAuthFail?: boolean
/**
* Enable authentication via a `session` option.
* @deprecated Will be removed in a future release
/**
* @deprecated This option should be set directly on the LegacySessionAuth
*/
useDeprecatedSessionAuth?: boolean
restartOnAuthFail?: boolean
/**
* WhatsApp session to restore. If not set, will start a new session
* @deprecated Set `useDeprecatedSessionAuth: true` to enable. This auth method is not supported by MultiDevice and will be removed in a future release.
* @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly.
*/
session?: ClientSession
/** Client id to distinguish instances if you are using multiple, otherwise keep empty if you are using only one instance
* @default '' */
clientId: string
/** If another whatsapp web session is detected (another browser), take over the session in the current browser
* @default false */
takeoverOnConflict?: boolean,
Expand All @@ -338,14 +325,53 @@ declare namespace WAWebJS {
/** Ffmpeg path to use when formating videos to webp while sending stickers
* @default 'ffmpeg' */
ffmpegPath?: string
/** Path to place session objects in
@default './WWebJS' */
dataPath?: string
}

/**
* Base class which all authentication strategies extend
*/
export abstract class AuthStrategy {
setup: (client: Client) => void;
beforeBrowserInitialized: () => Promise<void>;
afterBrowserInitialized: () => Promise<void>;
onAuthenticationNeeded: () => Promise<{
failed?: boolean;
restart?: boolean;
failureEventPayload?: any
}>;
getAuthEventPayload: () => Promise<any>;
logout: () => Promise<void>;
}

/**
* No session restoring functionality
* Will need to authenticate via QR code every time
*/
export class NoAuth extends AuthStrategy {}

/**
* Local directory-based authentication
*/
export class LocalAuth extends AuthStrategy {
constructor(options?: {
clientId?: string,
dataPath?: string
})
}

/**
* Legacy session auth strategy
* Not compatible with multi-device accounts.
*/
export class LegacySessionAuth extends AuthStrategy {
constructor(options?: {
session?: ClientSession,
restartOnAuth?: boolean,
})
}

/**
* Represents a WhatsApp client session
* @deprecated
*/
export interface ClientSession {
WABrowserId: string,
Expand Down
6 changes: 6 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ module.exports = {
ProductMetadata: require('./src/structures/ProductMetadata'),
List: require('./src/structures/List'),
Buttons: require('./src/structures/Buttons'),

// Auth Strategies
NoAuth: require('./src/authStrategies/NoAuth'),
LocalAuth: require('./src/authStrategies/LocalAuth'),
LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'),

...Constants
};
6 changes: 3 additions & 3 deletions shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
* ==== wwebjs-shell ====
* Used for quickly testing library features
*
* Running `npm run shell` will start WhatsApp Web in headless mode
* Running `npm run shell` will start WhatsApp Web with headless=false
* and then drop you into Node REPL with `client` in its context.
*/

const repl = require('repl');

const { Client } = require('./index');
const { Client, LocalAuth } = require('./index');

const client = new Client({
puppeteer: { headless: false },
clientId: 'shell'
authStrategy: new LocalAuth()
});

console.log('Initializing...');
Expand Down
108 changes: 37 additions & 71 deletions src/Client.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

const path = require('path');
const fs = require('fs');
const EventEmitter = require('events');
const puppeteer = require('puppeteer');
const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
Expand All @@ -13,23 +11,24 @@ const { ExposeStore, LoadUtils } = require('./util/Injected');
const ChatFactory = require('./factories/ChatFactory');
const ContactFactory = require('./factories/ContactFactory');
const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List } = require('./structures');
const LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
const NoAuth = require('./authStrategies/NoAuth');

/**
* Starting point for interacting with the WhatsApp Web API
* @extends {EventEmitter}
* @param {object} options - Client options
* @param {AuthStrategy} options.authStrategy - Determines how to save and restore sessions. Will use LegacySessionAuth if options.session is set. Otherwise, NoAuth will be used.
* @param {number} options.authTimeoutMs - Timeout for authentication selector in puppeteer
* @param {object} options.puppeteer - Puppeteer launch options. View docs here: https://github.com/puppeteer/puppeteer/
* @param {number} options.qrMaxRetries - How many times should the qrcode be refreshed before giving up
* @param {string} options.restartOnAuthFail - Restart client with a new session (i.e. use null 'session' var) if authentication fails
* @param {boolean} options.useDeprecatedSessionAuth - Enable JSON-based authentication. This is deprecated due to not being supported by MultiDevice, and will be removed in a future version.
* @param {object} options.session - This is deprecated due to not being supported by MultiDevice, and will be removed in a future version.
* @param {string} options.restartOnAuthFail - @deprecated This option should be set directly on the LegacySessionAuth.
* @param {object} options.session - @deprecated Only here for backwards-compatibility. You should move to using LocalAuth, or set the authStrategy to LegacySessionAuth explicitly.
* @param {number} options.takeoverOnConflict - If another whatsapp web session is detected (another browser), take over the session in the current browser
* @param {number} options.takeoverTimeoutMs - How much time to wait before taking over the session
* @param {string} options.dataPath - Change the default path for saving session files, default is: "./WWebJS/"
* @param {string} options.userAgent - User agent to use in puppeteer
* @param {string} options.ffmpegPath - Ffmpeg path to use when formating videos to webp while sending stickers
* @param {boolean} options.bypassCSP - Sets bypassing of page's Content-Security-Policy.
* @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
*
* @fires Client#qr
* @fires Client#authenticated
Expand All @@ -52,20 +51,28 @@ class Client extends EventEmitter {
super();

this.options = Util.mergeDefault(DefaultOptions, options);

this.id = this.options.clientId;

// eslint-disable-next-line no-useless-escape
const foldernameRegex = /^(?!.{256,})(?!(aux|clock\$|con|nul|prn|com[1-9]|lpt[1-9])(?:$|\.))[^ ][ \.\w-$()+=[\];#@~,&amp;']+[^\. ]$/i;
if (this.id && !foldernameRegex.test(this.id)) throw Error('Invalid client ID. Make sure you abide by the folder naming rules of your operating system.');

if (!this.options.useDeprecatedSessionAuth) {
this.dataDir = this.options.puppeteer.userDataDir;
const dirPath = path.join(process.cwd(), this.options.dataPath, this.id ? 'session-' + this.id : 'session');
if (!this.dataDir) this.dataDir = dirPath;
fs.mkdirSync(this.dataDir, { recursive: true });

if(!this.options.authStrategy) {
if(Object.prototype.hasOwnProperty.call(this.options, 'session')) {
process.emitWarning(
'options.session is deprecated and will be removed in a future release due to incompatibility with multi-device. ' +
'Use the LocalAuth authStrategy, don\'t pass in a session as an option, or suppress this warning by using the LegacySessionAuth strategy explicitly (see https://wwebjs.dev/guide/authentication.html#legacysessionauth-strategy).',
'DeprecationWarning'
);

this.authStrategy = new LegacySessionAuth({
session: this.options.session,
restartOnAuthFail: this.options.restartOnAuthFail
});
} else {
this.authStrategy = new NoAuth();
}
} else {
this.authStrategy = this.options.authStrategy;
}

this.authStrategy.setup(this);

this.pupBrowser = null;
this.pupPage = null;

Expand All @@ -78,10 +85,9 @@ class Client extends EventEmitter {
async initialize() {
let [browser, page] = [null, null];

const puppeteerOpts = {
...this.options.puppeteer,
userDataDir: this.options.useDeprecatedSessionAuth ? undefined : this.dataDir
};
await this.authStrategy.beforeBrowserInitialized();

const puppeteerOpts = this.options.puppeteer;
if (puppeteerOpts && puppeteerOpts.browserWSEndpoint) {
browser = await puppeteer.connect(puppeteerOpts);
page = await browser.newPage();
Expand All @@ -91,27 +97,12 @@ class Client extends EventEmitter {
}

await page.setUserAgent(this.options.userAgent);
if (this.options.bypassCSP) await page.setBypassCSP(true);

this.pupBrowser = browser;
this.pupPage = page;

if (this.options.useDeprecatedSessionAuth && this.options.session) {
await page.evaluateOnNewDocument(session => {
if (document.referrer === 'https://whatsapp.com/') {
localStorage.clear();
localStorage.setItem('WABrowserId', session.WABrowserId);
localStorage.setItem('WASecretBundle', session.WASecretBundle);
localStorage.setItem('WAToken1', session.WAToken1);
localStorage.setItem('WAToken2', session.WAToken2);
}

localStorage.setItem('remember-me', 'true');
}, this.options.session);
}

if (this.options.bypassCSP) {
await page.setBypassCSP(true);
}
await this.authStrategy.afterBrowserInitialized();

await page.goto(WhatsWebURL, {
waitUntil: 'load',
Expand Down Expand Up @@ -141,18 +132,17 @@ class Client extends EventEmitter {

// Scan-qrcode selector was found. Needs authentication
if (needAuthentication) {
if(this.options.session) {
const { failed, failureEventPayload, restart } = await this.authStrategy.onAuthenticationNeeded();
if(failed) {
/**
* Emitted when there has been an error while trying to restore an existing session
* @event Client#auth_failure
* @param {string} message
* @deprecated
*/
this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
await this.destroy();
if (this.options.restartOnAuthFail) {
if (restart) {
// session restore failed so try again but without session to force new authentication
this.options.session = null;
return this.initialize();
}
return;
Expand Down Expand Up @@ -224,20 +214,7 @@ class Client extends EventEmitter {
}

await page.evaluate(ExposeStore, moduleRaid.toString());
let authEventPayload = undefined;
if (this.options.useDeprecatedSessionAuth) {
// Get session tokens
const localStorage = JSON.parse(await page.evaluate(() => {
return JSON.stringify(window.localStorage);
}));

authEventPayload = {
WABrowserId: localStorage.WABrowserId,
WASecretBundle: localStorage.WASecretBundle,
WAToken1: localStorage.WAToken1,
WAToken2: localStorage.WAToken2
};
}
const authEventPayload = await this.authStrategy.getAuthEventPayload();

/**
* Emitted when authentication is successful
Expand All @@ -248,23 +225,14 @@ class Client extends EventEmitter {
// Check window.Store Injection
await page.waitForFunction('window.Store != undefined');

const isMD = await page.evaluate(() => {
return window.Store.Features.features.MD_BACKEND;
});

await page.evaluate(async () => {
// safely unregister service workers
const registrations = await navigator.serviceWorker.getRegistrations();
for (let registration of registrations) {
registration.unregister();
}

});

if (this.options.useDeprecatedSessionAuth && isMD) {
throw new Error('Authenticating via JSON session is not supported for MultiDevice-enabled WhatsApp accounts.');
}

//Load util functions (serializers, helper functions)
await page.evaluate(LoadUtils);

Expand Down Expand Up @@ -518,9 +486,7 @@ class Client extends EventEmitter {
return window.Store.AppState.logout();
});

if (this.dataDir) {
return (fs.rmSync ? fs.rmSync : fs.rmdirSync).call(this.dataDir, { recursive: true });
}
await this.authStrategy.logout();
}

/**
Expand Down

0 comments on commit f6de161

Please sign in to comment.