fix: Fallback to browser login when OAuth Device Flow fails
- epic-claimer-new.js: Try Device Flow first, fall back to email/password - Fixes: invalid_client_credentials error when device auth not configured - Users with EG_EMAIL/EG_PASSWORD can now use browser login as fallback - Device Flow remains available for users with valid credentials Behavior: 1. Try OAuth Device Flow (bypasses Cloudflare) 2. On failure (invalid credentials), use browser login 3. Browser login uses EG_EMAIL/EG_PASSWORD/EG_OTPKEY 4. Manual login via noVNC if both methods fail
This commit is contained in:
parent
393f70d409
commit
c8ccde9c22
1 changed files with 154 additions and 58 deletions
|
|
@ -103,26 +103,27 @@ const claimGame = async (page, game) => {
|
||||||
return notify_game;
|
return notify_game;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure user is logged in using OAuth Device Flow (bypasses Cloudflare)
|
// Ensure user is logged in - tries OAuth Device Flow first, falls back to browser login
|
||||||
const ensureLoggedIn = async (page, context) => {
|
const ensureLoggedIn = async (page, context) => {
|
||||||
const isLoggedIn = async () => await page.locator('egs-navigation').getAttribute('isloggedin') === 'true';
|
const isLoggedIn = async () => await page.locator('egs-navigation').getAttribute('isloggedin') === 'true';
|
||||||
const user = cfg.eg_email || 'default';
|
const user = cfg.eg_email || 'default';
|
||||||
|
|
||||||
L.info('Attempting OAuth Device Flow login (Cloudflare bypass)');
|
// Try OAuth Device Flow first (bypasses Cloudflare)
|
||||||
|
let useDeviceFlow = true;
|
||||||
|
let accessToken = null;
|
||||||
|
|
||||||
// Step 1: Try to get valid access token from stored device auth
|
try {
|
||||||
let accessToken = await getValidAccessToken(user);
|
L.info('Attempting OAuth Device Flow login (Cloudflare bypass)');
|
||||||
|
|
||||||
if (accessToken) {
|
// Step 1: Try to get valid access token from stored device auth
|
||||||
L.info('Using existing valid access token');
|
accessToken = await getValidAccessToken(user);
|
||||||
} else {
|
|
||||||
// Step 2: No valid token - start new device auth flow
|
if (accessToken) {
|
||||||
L.info('No valid token found, starting device auth flow');
|
L.info('Using existing valid access token');
|
||||||
await notify(
|
} else {
|
||||||
'epic-games: Login required! Visit the link to authorize: DEVICE_AUTH_PENDING',
|
// Step 2: No valid token - start new device auth flow
|
||||||
);
|
L.info('No valid token found, starting device auth flow');
|
||||||
|
|
||||||
try {
|
|
||||||
const { verificationUrl, userCode, expiresAt } = await startDeviceAuthLogin(user);
|
const { verificationUrl, userCode, expiresAt } = await startDeviceAuthLogin(user);
|
||||||
|
|
||||||
// Notify user with verification URL
|
// Notify user with verification URL
|
||||||
|
|
@ -140,8 +141,6 @@ const ensureLoggedIn = async (page, context) => {
|
||||||
// Wait for user to complete authorization
|
// Wait for user to complete authorization
|
||||||
const interval = 5; // poll every 5 seconds
|
const interval = 5; // poll every 5 seconds
|
||||||
const authToken = await completeDeviceAuthLogin(
|
const authToken = await completeDeviceAuthLogin(
|
||||||
// We need to get device_code from the startDeviceAuthLogin response
|
|
||||||
// For now, we'll re-fetch it
|
|
||||||
verificationUrl.split('userCode=')[1]?.split('&')[0] || '',
|
verificationUrl.split('userCode=')[1]?.split('&')[0] || '',
|
||||||
expiresAt,
|
expiresAt,
|
||||||
interval,
|
interval,
|
||||||
|
|
@ -149,64 +148,161 @@ const ensureLoggedIn = async (page, context) => {
|
||||||
|
|
||||||
accessToken = authToken.access_token;
|
accessToken = authToken.access_token;
|
||||||
L.info('Device auth completed successfully');
|
L.info('Device auth completed successfully');
|
||||||
} catch (err) {
|
|
||||||
L.error({ err }, 'Device auth flow failed');
|
|
||||||
await notify('epic-games: Device auth failed. Please login manually in browser.');
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 3: Apply bearer token to browser
|
||||||
|
L.info('Applying access token to browser');
|
||||||
|
|
||||||
|
/** @type {import('playwright-firefox').Cookie} */
|
||||||
|
const bearerCookie = {
|
||||||
|
name: 'EPIC_BEARER_TOKEN',
|
||||||
|
value: accessToken,
|
||||||
|
domain: '.epicgames.com',
|
||||||
|
path: '/',
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'Lax',
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.addCookies([bearerCookie]);
|
||||||
|
|
||||||
|
// Visit store to get session cookies
|
||||||
|
await page.goto(STORE_HOMEPAGE_EN, { waitUntil: 'networkidle', timeout: cfg.timeout });
|
||||||
|
|
||||||
|
// Verify login worked
|
||||||
|
const loggedIn = await isLoggedIn();
|
||||||
|
if (loggedIn) {
|
||||||
|
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
||||||
|
L.info({ user: displayName }, 'Successfully logged in via Device Flow');
|
||||||
|
console.log(`✅ Signed in as ${displayName} (OAuth Device Flow)`);
|
||||||
|
|
||||||
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
L.warn('Bearer token did not result in logged-in state');
|
||||||
|
useDeviceFlow = false;
|
||||||
|
} catch (err) {
|
||||||
|
L.warn({ err: err.message }, 'OAuth Device Flow failed, falling back to browser login');
|
||||||
|
console.log('⚠️ Device Auth failed:', err.message);
|
||||||
|
console.log('📝 Falling back to browser-based login with email/password...');
|
||||||
|
useDeviceFlow = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Apply bearer token to browser
|
// Fallback: Browser-based login with email/password
|
||||||
L.info('Applying access token to browser');
|
if (!useDeviceFlow) {
|
||||||
|
L.info('Using browser-based login (email/password)');
|
||||||
|
|
||||||
/** @type {import('playwright-firefox').Cookie} */
|
// Check if already logged in (from cookies)
|
||||||
const bearerCookie = {
|
if (await isLoggedIn()) {
|
||||||
name: 'EPIC_BEARER_TOKEN',
|
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
||||||
value: accessToken,
|
L.info({ user: displayName }, 'Already logged in (from cookies)');
|
||||||
domain: '.epicgames.com',
|
console.log(`✅ Already signed in as ${displayName}`);
|
||||||
path: '/',
|
return displayName;
|
||||||
secure: true,
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'Lax',
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.addCookies([bearerCookie]);
|
|
||||||
|
|
||||||
// Visit store to get session cookies
|
|
||||||
await page.goto(STORE_HOMEPAGE_EN, { waitUntil: 'networkidle', timeout: cfg.timeout });
|
|
||||||
|
|
||||||
// Verify login worked
|
|
||||||
const loggedIn = await isLoggedIn();
|
|
||||||
if (!loggedIn) {
|
|
||||||
L.warn('Bearer token did not result in logged-in state, may need manual login');
|
|
||||||
// Fall back to manual browser login
|
|
||||||
console.log('Token-based login did not work. Please login manually in the browser.');
|
|
||||||
await notify('epic-games: Please login manually in browser.');
|
|
||||||
|
|
||||||
if (cfg.headless) {
|
|
||||||
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
|
||||||
await context.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForTimeout(cfg.login_timeout);
|
// Try browser login
|
||||||
|
console.log('📝 Attempting browser login with email/password...');
|
||||||
|
const logged = await attemptBrowserLogin(page, context, isLoggedIn);
|
||||||
|
|
||||||
if (!await isLoggedIn()) {
|
if (!logged) {
|
||||||
throw new Error('Manual login did not complete within timeout');
|
L.error('Browser login failed');
|
||||||
|
console.log('❌ Browser login failed. Please login manually.');
|
||||||
|
await notify('epic-games: Login failed. Please login manually in browser.');
|
||||||
|
|
||||||
|
if (cfg.headless) {
|
||||||
|
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
||||||
|
await context.close();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Waiting for manual login in browser...');
|
||||||
|
await page.waitForTimeout(cfg.login_timeout);
|
||||||
|
|
||||||
|
if (!await isLoggedIn()) {
|
||||||
|
throw new Error('Login did not complete within timeout');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
||||||
|
L.info({ user: displayName }, 'Successfully logged in via browser');
|
||||||
|
console.log(`✅ Signed in as ${displayName} (Browser Login)`);
|
||||||
|
|
||||||
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
throw new Error('Login failed');
|
||||||
L.info({ user: displayName }, 'Successfully logged in');
|
};
|
||||||
console.log(`✅ Signed in as ${displayName}`);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
// Browser-based login helper function
|
||||||
return displayName;
|
const attemptBrowserLogin = async (page, context, isLoggedIn) => {
|
||||||
|
if (!cfg.eg_email || !cfg.eg_password) {
|
||||||
|
L.warn('No email/password configured');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(URL_LOGIN, {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: cfg.login_timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailField = page.locator('input[name="email"], input#email, input[aria-label="Sign in with email"]').first();
|
||||||
|
const passwordField = page.locator('input[name="password"], input#password').first();
|
||||||
|
const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first();
|
||||||
|
|
||||||
|
// Step 1: Email + continue
|
||||||
|
if (await emailField.count() > 0) {
|
||||||
|
await emailField.fill(cfg.eg_email);
|
||||||
|
await continueBtn.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Password + submit
|
||||||
|
await passwordField.waitFor({ timeout: cfg.login_visible_timeout });
|
||||||
|
await passwordField.fill(cfg.eg_password);
|
||||||
|
|
||||||
|
const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first();
|
||||||
|
if (await rememberMe.count() > 0) await rememberMe.check();
|
||||||
|
await continueBtn.click();
|
||||||
|
|
||||||
|
// MFA step
|
||||||
|
try {
|
||||||
|
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
||||||
|
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!',
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeInputs = page.locator('input[name^="code-input"]');
|
||||||
|
if (await codeInputs.count() > 0) {
|
||||||
|
const digits = otp.toString().split('');
|
||||||
|
for (let i = 0; i < digits.length; i++) {
|
||||||
|
const input = codeInputs.nth(i);
|
||||||
|
await input.fill(digits[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
|
||||||
|
}
|
||||||
|
await continueBtn.click();
|
||||||
|
} catch {
|
||||||
|
// No MFA
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForURL('**/free-games', { timeout: cfg.login_timeout });
|
||||||
|
return await isLoggedIn();
|
||||||
|
} catch (err) {
|
||||||
|
L.error({ err }, 'Browser login failed');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main function to claim Epic Games
|
// Main function to claim Epic Games
|
||||||
export const claimEpicGamesNew = async () => {
|
export const claimEpicGamesNew = async () => {
|
||||||
console.log('Starting Epic Games claimer (new mode, browser-based)');
|
console.log('Starting Epic Games claimer (new mode, cookies + API)');
|
||||||
const db = await jsonDb('epic-games.json', {});
|
const db = await jsonDb('epic-games.json', {});
|
||||||
const notify_games = [];
|
const notify_games = [];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue