add otplib to generate OTP from key for eg, pg; gog only has mail

This commit is contained in:
Ralf Vogler 2023-01-10 00:06:25 +01:00
parent 9e0d4434fe
commit 1a3d90f795
6 changed files with 141 additions and 3 deletions

View file

@ -60,8 +60,10 @@ Available options/variables and their default values:
| PASSWORD | | Default password for any login. | | PASSWORD | | Default password for any login. |
| EG_EMAIL | | Epic Games email for login. Overrides EMAIL. | | EG_EMAIL | | Epic Games email for login. Overrides EMAIL. |
| EG_PASSWORD | | Epic Games password for login. Overrides PASSWORD. | | EG_PASSWORD | | Epic Games password for login. Overrides PASSWORD. |
| EG_OTPKEY | | Epic Games MFA OTP key. |
| PG_EMAIL | | Prime Gaming email for login. Overrides EMAIL. | | PG_EMAIL | | Prime Gaming email for login. Overrides EMAIL. |
| PG_PASSWORD | | Prime Gaming password for login. Overrides PASSWORD. | | PG_PASSWORD | | Prime Gaming password for login. Overrides PASSWORD. |
| PG_OTPKEY | | Prime Gaming MFA OTP key. |
| GOG_EMAIL | | GOG email for login. Overrides EMAIL. | | GOG_EMAIL | | GOG email for login. Overrides EMAIL. |
| GOG_PASSWORD | | GOG password for login. Overrides PASSWORD. | | GOG_PASSWORD | | GOG password for login. Overrides PASSWORD. |
@ -71,6 +73,16 @@ See `config.js` for all options.
On Linux/macOS you can prefix the variables you want to set, for example `EMAIL=foo@bar.baz SHOW=1 node epic-games` will show the browser and skip asking you for your login email. On Linux/macOS you can prefix the variables you want to set, for example `EMAIL=foo@bar.baz SHOW=1 node epic-games` will show the browser and skip asking you for your login email.
For Docker you can pass variables using `-e VAR=VAL`, for example `docker run -e EMAIL=foo@bar.baz ...` or using `--env-file` (see [docs](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file)). If you are using [docker compose](https://docs.docker.com/compose/environment-variables/), you can put them in the `environment:` section. For Docker you can pass variables using `-e VAR=VAL`, for example `docker run -e EMAIL=foo@bar.baz ...` or using `--env-file` (see [docs](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file)). If you are using [docker compose](https://docs.docker.com/compose/environment-variables/), you can put them in the `environment:` section.
### Automatic login, two-factor authentication
If you set the options for email, password and OTP key, there will be no prompts and logins automatic. This is optional since all stores should stay logged in since cookies are refreshed.
To get the OTP key, it is easiest to follow the store's guide for adding an authenticator app. You should also scan the shown QR code with your favorite app to have an alternative.
- Epic Games: visit [password & security](https://www.epicgames.com/account/password), enable 'third-party authenticator app', copy the 'Manual Entry Key' and use it to set `EG_OTPKEY`.
- Prime Gaming: visit Amazon 'Your Account Login & security', 2-step verification Manage Add new app Can't scan the barcode, copy the bold key and use it to set `PG_OTPKEY`
- GOG: only offers OTP via email
Beware that storing passwords and OTP keys as clear text may be a security risk. Use a unique/generated password! TODO: maybe at least offer to base64 encode for storage.
### Epic Games Store ### Epic Games Store
Run `node epic-games` (locally or in Docker). Run `node epic-games` (locally or in Docker).

View file

@ -11,10 +11,15 @@ export const cfg = {
height: Number(process.env.HEIGHT) || 1280, // height of the opened browser height: Number(process.env.HEIGHT) || 1280, // height of the opened browser
timeout: (Number(process.env.TIMEOUT) || 20) * 1000, // 20s, default for playwright is 30s timeout: (Number(process.env.TIMEOUT) || 20) * 1000, // 20s, default for playwright is 30s
novnc_port: process.env.NOVNC_PORT, // running in docker if set novnc_port: process.env.NOVNC_PORT, // running in docker if set
// auth epic-games
eg_email: process.env.EG_EMAIL || process.env.EMAIL, eg_email: process.env.EG_EMAIL || process.env.EMAIL,
eg_password: process.env.EG_PASSWORD || process.env.PASSWORD, eg_password: process.env.EG_PASSWORD || process.env.PASSWORD,
eg_otpkey: process.env.EG_OTPKEY,
// auth prime-gaming
pg_email: process.env.PG_EMAIL || process.env.EMAIL, pg_email: process.env.PG_EMAIL || process.env.EMAIL,
pg_password: process.env.PG_PASSWORD || process.env.PASSWORD, pg_password: process.env.PG_PASSWORD || process.env.PASSWORD,
pg_otpkey: process.env.PG_OTPKEY,
// auth gog
gog_email: process.env.GOG_EMAIL || process.env.EMAIL, gog_email: process.env.GOG_EMAIL || process.env.EMAIL,
gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD, gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD,
}; };

View file

@ -1,8 +1,9 @@
import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib';
import path from 'path'; import path from 'path';
import { existsSync, writeFileSync } from 'fs';
import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js';
import { cfg } from './config.js'; import { cfg } from './config.js';
import { existsSync, writeFileSync } from 'fs';
import prompts from 'prompts'; // alternatives: enquirer, inquirer import prompts from 'prompts'; // alternatives: enquirer, inquirer
// import enquirer from 'enquirer'; const { prompt } = enquirer; // import enquirer from 'enquirer'; const { prompt } = enquirer;
@ -85,7 +86,7 @@ try {
// handle MFA, but don't await it // handle MFA, but don't await it
page.waitForNavigation({ url: '**/id/login/mfa**'}).then(async () => { page.waitForNavigation({ url: '**/id/login/mfa**'}).then(async () => {
console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...'); console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...');
const otp = await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them const otp = cfg.eg_otpkey && authenticator.generate(cfg.eg_otpkey) || await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them
await page.type('input[name="code-input-0"]', otp.toString()); await page.type('input[name="code-input-0"]', otp.toString());
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
}).catch(_ => { }); }).catch(_ => { });

118
package-lock.json generated
View file

@ -12,11 +12,54 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"lowdb": "^5.0.5", "lowdb": "^5.0.5",
"otplib": "^12.0.1",
"playwright": "^1.29.0", "playwright": "^1.29.0",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"puppeteer-extra-plugin-stealth": "^2.11.1" "puppeteer-extra-plugin-stealth": "^2.11.1"
} }
}, },
"node_modules/@otplib/core": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
"integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="
},
"node_modules/@otplib/plugin-crypto": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
"integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
"dependencies": {
"@otplib/core": "^12.0.1"
}
},
"node_modules/@otplib/plugin-thirty-two": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
"integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
"dependencies": {
"@otplib/core": "^12.0.1",
"thirty-two": "^1.0.2"
}
},
"node_modules/@otplib/preset-default": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
"integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
"dependencies": {
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"node_modules/@otplib/preset-v11": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
"integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
"dependencies": {
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"node_modules/@types/debug": { "node_modules/@types/debug": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -355,6 +398,16 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/otplib": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
"integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
"dependencies": {
"@otplib/core": "^12.0.1",
"@otplib/preset-default": "^12.0.1",
"@otplib/preset-v11": "^12.0.1"
}
},
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -593,6 +646,14 @@
"url": "https://github.com/sponsors/typicode" "url": "https://github.com/sponsors/typicode"
} }
}, },
"node_modules/thirty-two": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
"integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==",
"engines": {
"node": ">=0.2.6"
}
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@ -622,6 +683,48 @@
} }
}, },
"dependencies": { "dependencies": {
"@otplib/core": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
"integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA=="
},
"@otplib/plugin-crypto": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
"integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
"requires": {
"@otplib/core": "^12.0.1"
}
},
"@otplib/plugin-thirty-two": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
"integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
"requires": {
"@otplib/core": "^12.0.1",
"thirty-two": "^1.0.2"
}
},
"@otplib/preset-default": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
"integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
"requires": {
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"@otplib/preset-v11": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
"integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
"requires": {
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1"
}
},
"@types/debug": { "@types/debug": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@ -877,6 +980,16 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"otplib": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
"integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
"requires": {
"@otplib/core": "^12.0.1",
"@otplib/preset-default": "^12.0.1",
"@otplib/preset-v11": "^12.0.1"
}
},
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -1008,6 +1121,11 @@
"resolved": "https://registry.npmjs.org/steno/-/steno-3.0.0.tgz", "resolved": "https://registry.npmjs.org/steno/-/steno-3.0.0.tgz",
"integrity": "sha512-uZtn7Ht9yXLiYgOsmo8btj4+f7VxyYheMt8g6F1ANjyqByQXEE2Gygjgenp3otHH1TlHsS4JAaRGv5wJ1wvMNw==" "integrity": "sha512-uZtn7Ht9yXLiYgOsmo8btj4+f7VxyYheMt8g6F1ANjyqByQXEE2Gygjgenp3otHH1TlHsS4JAaRGv5wJ1wvMNw=="
}, },
"thirty-two": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
"integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA=="
},
"universalify": { "universalify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",

View file

@ -13,6 +13,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"lowdb": "^5.0.5", "lowdb": "^5.0.5",
"otplib": "^12.0.1",
"playwright": "^1.29.0", "playwright": "^1.29.0",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"puppeteer-extra-plugin-stealth": "^2.11.1" "puppeteer-extra-plugin-stealth": "^2.11.1"

View file

@ -1,4 +1,5 @@
import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib';
import path from 'path'; import path from 'path';
import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js';
import { cfg } from './config.js'; import { cfg } from './config.js';
@ -61,7 +62,7 @@ try {
page.waitForNavigation({ url: '**/ap/mfa**'}).then(async () => { page.waitForNavigation({ url: '**/ap/mfa**'}).then(async () => {
console.log('Two-Step Verification - enter the One Time Password (OTP), e.g. generated by your Authenticator App'); console.log('Two-Step Verification - enter the One Time Password (OTP), e.g. generated by your Authenticator App');
await page.check('[name=rememberDevice]'); await page.check('[name=rememberDevice]');
const otp = await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them const otp = cfg.pg_otpkey && authenticator.generate(cfg.pg_otpkey) || await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them
await page.type('input[name=otpCode]', otp.toString()); await page.type('input[name=otpCode]', otp.toString());
await page.click('input[type="submit"]'); await page.click('input[type="submit"]');
}); });