From 1a3d90f7953190a888865eafe32267fcba001cbd Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Tue, 10 Jan 2023 00:06:25 +0100 Subject: [PATCH] add otplib to generate OTP from key for eg, pg; gog only has mail --- README.md | 12 +++++ config.js | 5 ++ epic-games.js | 5 +- package-lock.json | 118 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + prime-gaming.js | 3 +- 6 files changed, 141 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 49d06ac..b14c0ba 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,10 @@ Available options/variables and their default values: | PASSWORD | | Default password for any login. | | EG_EMAIL | | Epic Games email for login. Overrides EMAIL. | | 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_PASSWORD | | Prime Gaming password for login. Overrides PASSWORD. | +| PG_OTPKEY | | Prime Gaming MFA OTP key. | | GOG_EMAIL | | GOG email for login. Overrides EMAIL. | | 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. 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 Run `node epic-games` (locally or in Docker). diff --git a/config.js b/config.js index e0873b5..e6d2cd6 100644 --- a/config.js +++ b/config.js @@ -11,10 +11,15 @@ export const cfg = { height: Number(process.env.HEIGHT) || 1280, // height of the opened browser timeout: (Number(process.env.TIMEOUT) || 20) * 1000, // 20s, default for playwright is 30s novnc_port: process.env.NOVNC_PORT, // running in docker if set + // auth epic-games eg_email: process.env.EG_EMAIL || process.env.EMAIL, 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_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_password: process.env.GOG_PASSWORD || process.env.PASSWORD, }; diff --git a/epic-games.js b/epic-games.js index fb1a604..4f40525 100644 --- a/epic-games.js +++ b/epic-games.js @@ -1,8 +1,9 @@ import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra +import { authenticator } from 'otplib'; import path from 'path'; +import { existsSync, writeFileSync } from 'fs'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; import { cfg } from './config.js'; -import { existsSync, writeFileSync } from 'fs'; import prompts from 'prompts'; // alternatives: enquirer, inquirer // import enquirer from 'enquirer'; const { prompt } = enquirer; @@ -85,7 +86,7 @@ try { // handle MFA, but don't await it 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 ...'); - 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.click('button[type="submit"]'); }).catch(_ => { }); diff --git a/package-lock.json b/package-lock.json index 5f5954b..3280933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,54 @@ "cross-env": "^7.0.3", "dotenv": "^16.0.3", "lowdb": "^5.0.5", + "otplib": "^12.0.1", "playwright": "^1.29.0", "prompts": "^2.4.2", "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": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -355,6 +398,16 @@ "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": { "version": "1.0.1", "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" } }, + "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -622,6 +683,48 @@ } }, "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": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -877,6 +980,16 @@ "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": { "version": "1.0.1", "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", "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 7720bf5..6f90c03 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "cross-env": "^7.0.3", "dotenv": "^16.0.3", "lowdb": "^5.0.5", + "otplib": "^12.0.1", "playwright": "^1.29.0", "prompts": "^2.4.2", "puppeteer-extra-plugin-stealth": "^2.11.1" diff --git a/prime-gaming.js b/prime-gaming.js index 940cc9e..ee07acb 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -1,4 +1,5 @@ import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra +import { authenticator } from 'otplib'; import path from 'path'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; import { cfg } from './config.js'; @@ -61,7 +62,7 @@ try { 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'); 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.click('input[type="submit"]'); });