mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-28 10:51:03 +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
|
.DS_Store
|
||||||
.playwright-chromium-installed
|
.playwright-chromium-installed
|
||||||
bun.lock
|
bun.lock
|
||||||
|
logs/
|
||||||
|
.stfolder/
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type LoginState =
|
|||||||
| 'LOGIN_PASSWORDLESS'
|
| 'LOGIN_PASSWORDLESS'
|
||||||
| 'GET_A_CODE'
|
| 'GET_A_CODE'
|
||||||
| 'GET_A_CODE_2'
|
| 'GET_A_CODE_2'
|
||||||
|
| 'OTP_CODE_ENTRY'
|
||||||
| 'UNKNOWN'
|
| 'UNKNOWN'
|
||||||
| 'CHROMEWEBDATA_ERROR'
|
| 'CHROMEWEBDATA_ERROR'
|
||||||
|
|
||||||
@@ -56,9 +57,13 @@ export class Login {
|
|||||||
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
|
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
|
||||||
identityBanner: '[data-testid="identityBanner"]',
|
identityBanner: '[data-testid="identityBanner"]',
|
||||||
viewFooter: '[data-testid="viewFooter"] >> [role="button"]',
|
viewFooter: '[data-testid="viewFooter"] >> [role="button"]',
|
||||||
|
otherWaysToSignIn: '[data-testid="viewFooter"] span[role="button"]',
|
||||||
|
otpCodeEntry: '[data-testid="codeEntry"]',
|
||||||
|
backButton: '#back-button',
|
||||||
bingProfile: '#id_n',
|
bingProfile: '#id_n',
|
||||||
requestToken: 'input[name="__RequestVerificationToken"]',
|
requestToken: 'input[name="__RequestVerificationToken"]',
|
||||||
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
|
requestTokenMeta: 'meta[name="__RequestVerificationToken"]',
|
||||||
|
otpInput: 'div[data-testid="codeEntry"]'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
constructor(private bot: MicrosoftRewardsBot) {
|
constructor(private bot: MicrosoftRewardsBot) {
|
||||||
@@ -73,7 +78,7 @@ export class Login {
|
|||||||
try {
|
try {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
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.utils.wait(2000)
|
||||||
await this.bot.browser.utils.reloadBadPage(page)
|
await this.bot.browser.utils.reloadBadPage(page)
|
||||||
await this.bot.browser.utils.disableFido(page)
|
await this.bot.browser.utils.disableFido(page)
|
||||||
@@ -149,7 +154,7 @@ export class Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
|
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())
|
const url = new URL(page.url())
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
|
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.emailIconOld, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
|
||||||
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
|
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
|
||||||
[this.selectors.totpInput, '2FA_TOTP'],
|
[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(
|
const results = await Promise.all(
|
||||||
@@ -243,8 +250,11 @@ export class Login {
|
|||||||
'KMSI_PROMPT',
|
'KMSI_PROMPT',
|
||||||
'PASSWORD_INPUT',
|
'PASSWORD_INPUT',
|
||||||
'EMAIL_INPUT',
|
'EMAIL_INPUT',
|
||||||
|
'SIGN_IN_ANOTHER_WAY', // Prefer password option over email code
|
||||||
'SIGN_IN_ANOTHER_WAY_EMAIL',
|
'SIGN_IN_ANOTHER_WAY_EMAIL',
|
||||||
'SIGN_IN_ANOTHER_WAY',
|
'OTP_CODE_ENTRY',
|
||||||
|
'GET_A_CODE',
|
||||||
|
'GET_A_CODE_2',
|
||||||
'LOGIN_PASSWORDLESS',
|
'LOGIN_PASSWORDLESS',
|
||||||
'2FA_TOTP'
|
'2FA_TOTP'
|
||||||
]
|
]
|
||||||
@@ -308,12 +318,56 @@ export class Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'GET_A_CODE': {
|
case 'GET_A_CODE': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" page')
|
||||||
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
|
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
// Try to find "Other ways to sign in" link
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
|
const otherWaysLink = await page
|
||||||
})
|
.waitForSelector(this.selectors.otherWaysToSignIn, { state: 'visible', timeout: 3000 })
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer clicked, proceeding')
|
.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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +435,7 @@ export class Login {
|
|||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => { })
|
||||||
await this.bot.utils.wait(3000)
|
await this.bot.utils.wait(3000)
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
|
||||||
return true
|
return true
|
||||||
@@ -392,7 +446,7 @@ export class Login {
|
|||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => { })
|
||||||
await this.bot.utils.wait(3000)
|
await this.bot.utils.wait(3000)
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
|
||||||
return true
|
return true
|
||||||
@@ -447,6 +501,38 @@ export class Login {
|
|||||||
return true
|
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': {
|
case 'UNKNOWN': {
|
||||||
const url = new URL(page.url())
|
const url = new URL(page.url())
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
@@ -466,7 +552,7 @@ export class Login {
|
|||||||
private async finalizeLogin(page: Page, email: string) {
|
private async finalizeLogin(page: Page, email: string) {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Finalizing login')
|
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'
|
const loginRewardsSuccess = new URL(page.url()).hostname === 'rewards.bing.com'
|
||||||
if (loginRewardsSuccess) {
|
if (loginRewardsSuccess) {
|
||||||
@@ -497,7 +583,7 @@ export class Login {
|
|||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing session')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing session')
|
||||||
|
|
||||||
try {
|
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++) {
|
for (let i = 0; i < loopMax; i++) {
|
||||||
if (page.isClosed()) break
|
if (page.isClosed()) break
|
||||||
@@ -519,7 +605,7 @@ export class Login {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (atBingHome) {
|
if (atBingHome) {
|
||||||
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
|
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => { })
|
||||||
|
|
||||||
const signedIn = await page
|
const signedIn = await page
|
||||||
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
|
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
|
||||||
@@ -555,7 +641,7 @@ export class Login {
|
|||||||
try {
|
try {
|
||||||
await page
|
await page
|
||||||
.goto(`${this.bot.config.baseURL}?_=${Date.now()}`, { waitUntil: 'networkidle', timeout: 10000 })
|
.goto(`${this.bot.config.baseURL}?_=${Date.now()}`, { waitUntil: 'networkidle', timeout: 10000 })
|
||||||
.catch(() => {})
|
.catch(() => { })
|
||||||
|
|
||||||
for (let i = 0; i < loopMax; i++) {
|
for (let i = 0; i < loopMax; i++) {
|
||||||
if (page.isClosed()) break
|
if (page.isClosed()) break
|
||||||
|
|||||||
@@ -12,11 +12,40 @@ export class MobileAccessLogin {
|
|||||||
private scope = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
|
private scope = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
|
||||||
private maxTimeout = 180_000 // 3min
|
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(
|
constructor(
|
||||||
private bot: MicrosoftRewardsBot,
|
private bot: MicrosoftRewardsBot,
|
||||||
private page: Page
|
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> {
|
async get(email: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const authorizeUrl = new URL(this.authUrl)
|
const authorizeUrl = new URL(this.authUrl)
|
||||||
@@ -72,6 +101,9 @@ export class MobileAccessLogin {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Passkey prompt if it appears
|
||||||
|
await this.handlePasskeyPrompt()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.bot.logger.debug(
|
this.bot.logger.debug(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function formatMessage(message: string | Error): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
constructor(private bot: MicrosoftRewardsBot) {}
|
constructor(private bot: MicrosoftRewardsBot) { }
|
||||||
|
|
||||||
info(isMobile: Platform, title: string, message: string, color?: ColorKey) {
|
info(isMobile: Platform, title: string, message: string, color?: ColorKey) {
|
||||||
return this.baseLog('info', isMobile, title, message, color)
|
return this.baseLog('info', isMobile, title, message, color)
|
||||||
@@ -180,7 +180,7 @@ export class Logger {
|
|||||||
isMatch = true
|
isMatch = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user