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:
Simon Gardling
2026-01-19 04:26:19 -05:00
committed by GitHub
parent b0c01fd433
commit 7e51bff52b
4 changed files with 139 additions and 19 deletions

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@ accounts.main.json
.DS_Store
.playwright-chromium-installed
bun.lock
logs/
.stfolder/

View File

@@ -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

View File

@@ -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,

View File

@@ -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 { }
}
}