mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-19 22:43:58 +00:00
Fix OTP/passkey login flow handling (#450)
Merge of various other commits regarding fixing OTP/passkey handling and the bot getting lost in the login flow. Co-authored-by: eejay <eejay@eejay-2.local>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,3 +12,5 @@ accounts.main.json
|
||||
.DS_Store
|
||||
.playwright-chromium-installed
|
||||
bun.lock
|
||||
logs/
|
||||
.stfolder/
|
||||
|
||||
@@ -27,6 +27,7 @@ type LoginState =
|
||||
| 'LOGIN_PASSWORDLESS'
|
||||
| 'GET_A_CODE'
|
||||
| 'GET_A_CODE_2'
|
||||
| 'OTP_CODE_ENTRY'
|
||||
| 'UNKNOWN'
|
||||
| 'CHROMEWEBDATA_ERROR'
|
||||
|
||||
@@ -56,9 +57,13 @@ export class Login {
|
||||
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
|
||||
identityBanner: '[data-testid="identityBanner"]',
|
||||
viewFooter: '[data-testid="viewFooter"] >> [role="button"]',
|
||||
otherWaysToSignIn: '[data-testid="viewFooter"] span[role="button"]',
|
||||
otpCodeEntry: '[data-testid="codeEntry"]',
|
||||
backButton: '#back-button',
|
||||
bingProfile: '#id_n',
|
||||
requestToken: 'input[name="__RequestVerificationToken"]',
|
||||
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
|
||||
requestTokenMeta: 'meta[name="__RequestVerificationToken"]',
|
||||
otpInput: 'div[data-testid="codeEntry"]'
|
||||
} as const
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {
|
||||
@@ -73,7 +78,7 @@ export class Login {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
||||
|
||||
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
|
||||
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => { })
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
await this.bot.browser.utils.disableFido(page)
|
||||
@@ -149,7 +154,7 @@ export class Login {
|
||||
}
|
||||
|
||||
private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
|
||||
|
||||
const url = new URL(page.url())
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
|
||||
@@ -183,7 +188,9 @@ export class Login {
|
||||
[this.selectors.emailIconOld, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
|
||||
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
|
||||
[this.selectors.totpInput, '2FA_TOTP'],
|
||||
[this.selectors.totpInputOld, '2FA_TOTP']
|
||||
[this.selectors.totpInputOld, '2FA_TOTP'],
|
||||
[this.selectors.otpCodeEntry, 'OTP_CODE_ENTRY'], // PR 450
|
||||
[this.selectors.otpInput, 'OTP_CODE_ENTRY'] // My Fix
|
||||
]
|
||||
|
||||
const results = await Promise.all(
|
||||
@@ -243,8 +250,11 @@ export class Login {
|
||||
'KMSI_PROMPT',
|
||||
'PASSWORD_INPUT',
|
||||
'EMAIL_INPUT',
|
||||
'SIGN_IN_ANOTHER_WAY', // Prefer password option over email code
|
||||
'SIGN_IN_ANOTHER_WAY_EMAIL',
|
||||
'SIGN_IN_ANOTHER_WAY',
|
||||
'OTP_CODE_ENTRY',
|
||||
'GET_A_CODE',
|
||||
'GET_A_CODE_2',
|
||||
'LOGIN_PASSWORDLESS',
|
||||
'2FA_TOTP'
|
||||
]
|
||||
@@ -308,12 +318,56 @@ export class Login {
|
||||
}
|
||||
|
||||
case 'GET_A_CODE': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer clicked, proceeding')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" page')
|
||||
|
||||
// Try to find "Other ways to sign in" link
|
||||
const otherWaysLink = await page
|
||||
.waitForSelector(this.selectors.otherWaysToSignIn, { state: 'visible', timeout: 3000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (otherWaysLink) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Found "Other ways to sign in" link')
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.otherWaysToSignIn)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
'Network idle timeout after clicking other ways'
|
||||
)
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', '"Other ways to sign in" clicked')
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback: try the generic viewFooter selector
|
||||
const footerLink = await page
|
||||
.waitForSelector(this.selectors.viewFooter, { state: 'visible', timeout: 2000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (footerLink) {
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer link clicked')
|
||||
return true
|
||||
}
|
||||
|
||||
// If no links found, try clicking back button
|
||||
const backBtn = await page
|
||||
.waitForSelector(this.selectors.backButton, { state: 'visible', timeout: 2000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (backBtn) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'No sign in options found, clicking back button')
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.backButton)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after back button')
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Could not find way to bypass Get Code page')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -381,7 +435,7 @@ export class Login {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 10000
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
await this.bot.utils.wait(3000)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
|
||||
return true
|
||||
@@ -392,7 +446,7 @@ export class Login {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 10000
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
await this.bot.utils.wait(3000)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
|
||||
return true
|
||||
@@ -447,6 +501,38 @@ export class Login {
|
||||
return true
|
||||
}
|
||||
|
||||
case 'OTP_CODE_ENTRY': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'OTP code entry page detected, attempting to find password option')
|
||||
|
||||
// My Fix: Click "Use your password" footer
|
||||
const footerLink = await page
|
||||
.waitForSelector(this.selectors.viewFooter, { state: 'visible', timeout: 2000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (footerLink) {
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer link clicked')
|
||||
} else {
|
||||
// PR 450 Fix: Click Back Button if footer not found
|
||||
const backButton = await page
|
||||
.waitForSelector(this.selectors.backButton, { state: 'visible', timeout: 2000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (backButton) {
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.backButton)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Back button clicked')
|
||||
} else {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'No navigation option found on OTP page')
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after OTP navigation')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Navigated back from OTP entry page')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'UNKNOWN': {
|
||||
const url = new URL(page.url())
|
||||
this.bot.logger.warn(
|
||||
@@ -466,7 +552,7 @@ export class Login {
|
||||
private async finalizeLogin(page: Page, email: string) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Finalizing login')
|
||||
|
||||
await page.goto(this.bot.config.baseURL, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {})
|
||||
await page.goto(this.bot.config.baseURL, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => { })
|
||||
|
||||
const loginRewardsSuccess = new URL(page.url()).hostname === 'rewards.bing.com'
|
||||
if (loginRewardsSuccess) {
|
||||
@@ -497,7 +583,7 @@ export class Login {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing session')
|
||||
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {})
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => { })
|
||||
|
||||
for (let i = 0; i < loopMax; i++) {
|
||||
if (page.isClosed()) break
|
||||
@@ -519,7 +605,7 @@ export class Login {
|
||||
)
|
||||
|
||||
if (atBingHome) {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => { })
|
||||
|
||||
const signedIn = await page
|
||||
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
|
||||
@@ -555,7 +641,7 @@ export class Login {
|
||||
try {
|
||||
await page
|
||||
.goto(`${this.bot.config.baseURL}?_=${Date.now()}`, { waitUntil: 'networkidle', timeout: 10000 })
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
|
||||
for (let i = 0; i < loopMax; i++) {
|
||||
if (page.isClosed()) break
|
||||
|
||||
@@ -12,11 +12,40 @@ export class MobileAccessLogin {
|
||||
private scope = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
|
||||
private maxTimeout = 180_000 // 3min
|
||||
|
||||
// Selectors for handling Passkey prompt during OAuth
|
||||
private readonly selectors = {
|
||||
secondaryButton: 'button[data-testid="secondaryButton"]',
|
||||
passKeyError: '[data-testid="registrationImg"]',
|
||||
passKeyVideo: '[data-testid="biometricVideo"]'
|
||||
} as const
|
||||
|
||||
constructor(
|
||||
private bot: MicrosoftRewardsBot,
|
||||
private page: Page
|
||||
) {}
|
||||
|
||||
private async checkSelector(selector: string): Promise<boolean> {
|
||||
return this.page
|
||||
.waitForSelector(selector, { state: 'visible', timeout: 200 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
private async handlePasskeyPrompt(): Promise<void> {
|
||||
try {
|
||||
// Handle Passkey prompt - click secondary button to skip
|
||||
const hasPasskeyError = await this.checkSelector(this.selectors.passKeyError)
|
||||
const hasPasskeyVideo = await this.checkSelector(this.selectors.passKeyVideo)
|
||||
if (hasPasskeyError || hasPasskeyVideo) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Found Passkey prompt on OAuth page, skipping')
|
||||
await this.bot.browser.utils.ghostClick(this.page, this.selectors.secondaryButton)
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors in prompt handling
|
||||
}
|
||||
}
|
||||
|
||||
async get(email: string): Promise<string> {
|
||||
try {
|
||||
const authorizeUrl = new URL(this.authUrl)
|
||||
@@ -72,6 +101,9 @@ export class MobileAccessLogin {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Passkey prompt if it appears
|
||||
await this.handlePasskeyPrompt()
|
||||
} catch (err) {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
|
||||
@@ -45,7 +45,7 @@ function formatMessage(message: string | Error): string {
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
constructor(private bot: MicrosoftRewardsBot) { }
|
||||
|
||||
info(isMobile: Platform, title: string, message: string, color?: ColorKey) {
|
||||
return this.baseLog('info', isMobile, title, message, color)
|
||||
@@ -180,7 +180,7 @@ export class Logger {
|
||||
isMatch = true
|
||||
break
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user