Skip to content

Commit c8e4149

Browse files
Titaniumtowneejay
andcommitted
Fix OTP/passkey login flow handling
Merge of various other commits regarding fixing OTP/passkey handling and the bot getting lost in the login flow. Co-authored-by: eejay <[email protected]>
1 parent b0c01fd commit c8e4149

File tree

4 files changed

+139
-19
lines changed

4 files changed

+139
-19
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ accounts.main.json
1212
.DS_Store
1313
.playwright-chromium-installed
1414
bun.lock
15+
logs/
16+
.stfolder/

src/browser/auth/Login.ts

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type LoginState =
2727
| 'LOGIN_PASSWORDLESS'
2828
| 'GET_A_CODE'
2929
| 'GET_A_CODE_2'
30+
| 'OTP_CODE_ENTRY'
3031
| 'UNKNOWN'
3132
| 'CHROMEWEBDATA_ERROR'
3233

@@ -56,9 +57,13 @@ export class Login {
5657
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
5758
identityBanner: '[data-testid="identityBanner"]',
5859
viewFooter: '[data-testid="viewFooter"] >> [role="button"]',
60+
otherWaysToSignIn: '[data-testid="viewFooter"] span[role="button"]',
61+
otpCodeEntry: '[data-testid="codeEntry"]',
62+
backButton: '#back-button',
5963
bingProfile: '#id_n',
6064
requestToken: 'input[name="__RequestVerificationToken"]',
61-
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
65+
requestTokenMeta: 'meta[name="__RequestVerificationToken"]',
66+
otpInput: 'div[data-testid="codeEntry"]'
6267
} as const
6368

6469
constructor(private bot: MicrosoftRewardsBot) {
@@ -73,7 +78,7 @@ export class Login {
7378
try {
7479
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
7580

76-
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
81+
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => { })
7782
await this.bot.utils.wait(2000)
7883
await this.bot.browser.utils.reloadBadPage(page)
7984
await this.bot.browser.utils.disableFido(page)
@@ -149,7 +154,7 @@ export class Login {
149154
}
150155

151156
private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
152-
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
157+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
153158

154159
const url = new URL(page.url())
155160
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
@@ -183,7 +188,9 @@ export class Login {
183188
[this.selectors.emailIconOld, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
184189
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
185190
[this.selectors.totpInput, '2FA_TOTP'],
186-
[this.selectors.totpInputOld, '2FA_TOTP']
191+
[this.selectors.totpInputOld, '2FA_TOTP'],
192+
[this.selectors.otpCodeEntry, 'OTP_CODE_ENTRY'], // PR 450
193+
[this.selectors.otpInput, 'OTP_CODE_ENTRY'] // My Fix
187194
]
188195

189196
const results = await Promise.all(
@@ -243,8 +250,11 @@ export class Login {
243250
'KMSI_PROMPT',
244251
'PASSWORD_INPUT',
245252
'EMAIL_INPUT',
253+
'SIGN_IN_ANOTHER_WAY', // Prefer password option over email code
246254
'SIGN_IN_ANOTHER_WAY_EMAIL',
247-
'SIGN_IN_ANOTHER_WAY',
255+
'OTP_CODE_ENTRY',
256+
'GET_A_CODE',
257+
'GET_A_CODE_2',
248258
'LOGIN_PASSWORDLESS',
249259
'2FA_TOTP'
250260
]
@@ -308,12 +318,56 @@ export class Login {
308318
}
309319

310320
case 'GET_A_CODE': {
311-
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
312-
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
313-
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
314-
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
315-
})
316-
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer clicked, proceeding')
321+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" page')
322+
323+
// Try to find "Other ways to sign in" link
324+
const otherWaysLink = await page
325+
.waitForSelector(this.selectors.otherWaysToSignIn, { state: 'visible', timeout: 3000 })
326+
.catch(() => null)
327+
328+
if (otherWaysLink) {
329+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Found "Other ways to sign in" link')
330+
await this.bot.browser.utils.ghostClick(page, this.selectors.otherWaysToSignIn)
331+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
332+
this.bot.logger.debug(
333+
this.bot.isMobile,
334+
'LOGIN',
335+
'Network idle timeout after clicking other ways'
336+
)
337+
})
338+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', '"Other ways to sign in" clicked')
339+
return true
340+
}
341+
342+
// Fallback: try the generic viewFooter selector
343+
const footerLink = await page
344+
.waitForSelector(this.selectors.viewFooter, { state: 'visible', timeout: 2000 })
345+
.catch(() => null)
346+
347+
if (footerLink) {
348+
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
349+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
350+
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
351+
})
352+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer link clicked')
353+
return true
354+
}
355+
356+
// If no links found, try clicking back button
357+
const backBtn = await page
358+
.waitForSelector(this.selectors.backButton, { state: 'visible', timeout: 2000 })
359+
.catch(() => null)
360+
361+
if (backBtn) {
362+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'No sign in options found, clicking back button')
363+
await this.bot.browser.utils.ghostClick(page, this.selectors.backButton)
364+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
365+
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after back button')
366+
})
367+
return true
368+
}
369+
370+
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Could not find way to bypass Get Code page')
317371
return true
318372
}
319373

@@ -381,7 +435,7 @@ export class Login {
381435
waitUntil: 'domcontentloaded',
382436
timeout: 10000
383437
})
384-
.catch(() => {})
438+
.catch(() => { })
385439
await this.bot.utils.wait(3000)
386440
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
387441
return true
@@ -392,7 +446,7 @@ export class Login {
392446
waitUntil: 'domcontentloaded',
393447
timeout: 10000
394448
})
395-
.catch(() => {})
449+
.catch(() => { })
396450
await this.bot.utils.wait(3000)
397451
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
398452
return true
@@ -447,6 +501,38 @@ export class Login {
447501
return true
448502
}
449503

504+
case 'OTP_CODE_ENTRY': {
505+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'OTP code entry page detected, attempting to find password option')
506+
507+
// My Fix: Click "Use your password" footer
508+
const footerLink = await page
509+
.waitForSelector(this.selectors.viewFooter, { state: 'visible', timeout: 2000 })
510+
.catch(() => null)
511+
512+
if (footerLink) {
513+
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
514+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer link clicked')
515+
} else {
516+
// PR 450 Fix: Click Back Button if footer not found
517+
const backButton = await page
518+
.waitForSelector(this.selectors.backButton, { state: 'visible', timeout: 2000 })
519+
.catch(() => null)
520+
521+
if (backButton) {
522+
await this.bot.browser.utils.ghostClick(page, this.selectors.backButton)
523+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Back button clicked')
524+
} else {
525+
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'No navigation option found on OTP page')
526+
}
527+
}
528+
529+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
530+
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after OTP navigation')
531+
})
532+
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Navigated back from OTP entry page')
533+
return true
534+
}
535+
450536
case 'UNKNOWN': {
451537
const url = new URL(page.url())
452538
this.bot.logger.warn(
@@ -466,7 +552,7 @@ export class Login {
466552
private async finalizeLogin(page: Page, email: string) {
467553
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Finalizing login')
468554

469-
await page.goto(this.bot.config.baseURL, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {})
555+
await page.goto(this.bot.config.baseURL, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => { })
470556

471557
const loginRewardsSuccess = new URL(page.url()).hostname === 'rewards.bing.com'
472558
if (loginRewardsSuccess) {
@@ -497,7 +583,7 @@ export class Login {
497583
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing session')
498584

499585
try {
500-
await page.goto(url, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {})
586+
await page.goto(url, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => { })
501587

502588
for (let i = 0; i < loopMax; i++) {
503589
if (page.isClosed()) break
@@ -519,7 +605,7 @@ export class Login {
519605
)
520606

521607
if (atBingHome) {
522-
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
608+
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => { })
523609

524610
const signedIn = await page
525611
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
@@ -555,7 +641,7 @@ export class Login {
555641
try {
556642
await page
557643
.goto(`${this.bot.config.baseURL}?_=${Date.now()}`, { waitUntil: 'networkidle', timeout: 10000 })
558-
.catch(() => {})
644+
.catch(() => { })
559645

560646
for (let i = 0; i < loopMax; i++) {
561647
if (page.isClosed()) break

src/browser/auth/methods/MobileAccessLogin.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,40 @@ export class MobileAccessLogin {
1212
private scope = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
1313
private maxTimeout = 180_000 // 3min
1414

15+
// Selectors for handling Passkey prompt during OAuth
16+
private readonly selectors = {
17+
secondaryButton: 'button[data-testid="secondaryButton"]',
18+
passKeyError: '[data-testid="registrationImg"]',
19+
passKeyVideo: '[data-testid="biometricVideo"]'
20+
} as const
21+
1522
constructor(
1623
private bot: MicrosoftRewardsBot,
1724
private page: Page
1825
) {}
1926

27+
private async checkSelector(selector: string): Promise<boolean> {
28+
return this.page
29+
.waitForSelector(selector, { state: 'visible', timeout: 200 })
30+
.then(() => true)
31+
.catch(() => false)
32+
}
33+
34+
private async handlePasskeyPrompt(): Promise<void> {
35+
try {
36+
// Handle Passkey prompt - click secondary button to skip
37+
const hasPasskeyError = await this.checkSelector(this.selectors.passKeyError)
38+
const hasPasskeyVideo = await this.checkSelector(this.selectors.passKeyVideo)
39+
if (hasPasskeyError || hasPasskeyVideo) {
40+
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Found Passkey prompt on OAuth page, skipping')
41+
await this.bot.browser.utils.ghostClick(this.page, this.selectors.secondaryButton)
42+
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
43+
}
44+
} catch {
45+
// Ignore errors in prompt handling
46+
}
47+
}
48+
2049
async get(email: string): Promise<string> {
2150
try {
2251
const authorizeUrl = new URL(this.authUrl)
@@ -72,6 +101,9 @@ export class MobileAccessLogin {
72101
break
73102
}
74103
}
104+
105+
// Handle Passkey prompt if it appears
106+
await this.handlePasskeyPrompt()
75107
} catch (err) {
76108
this.bot.logger.debug(
77109
this.bot.isMobile,

src/logging/Logger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function formatMessage(message: string | Error): string {
4545
}
4646

4747
export class Logger {
48-
constructor(private bot: MicrosoftRewardsBot) {}
48+
constructor(private bot: MicrosoftRewardsBot) { }
4949

5050
info(isMobile: Platform, title: string, message: string, color?: ColorKey) {
5151
return this.baseLog('info', isMobile, title, message, color)
@@ -180,7 +180,7 @@ export class Logger {
180180
isMatch = true
181181
break
182182
}
183-
} catch {}
183+
} catch { }
184184
}
185185
}
186186

0 commit comments

Comments
 (0)