mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
feat: Implement security handling for login process
- Added PasskeyHandler to manage passkey prompts and interactions. - Introduced RecoveryHandler for handling recovery email mismatches. - Created SecurityDetector to identify sign-in blocks and account locks. - Developed SecurityUtils for incident reporting and documentation access. - Implemented TotpHandler for managing two-factor authentication (2FA) processes. - Defined SecurityIncident type for structured incident reporting.
This commit is contained in:
204
src/functions/login/PasskeyHandler.ts
Normal file
204
src/functions/login/PasskeyHandler.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { Page } from 'playwright'
|
||||
import { MicrosoftRewardsBot } from '../../index'
|
||||
import { waitForElementSmart } from '../../util/browser/SmartWait'
|
||||
import { logError } from '../../util/notifications/Logger'
|
||||
|
||||
export class PasskeyHandler {
|
||||
private bot: MicrosoftRewardsBot
|
||||
private passkeyHandled = false
|
||||
private noPromptIterations = 0
|
||||
private lastNoPromptLog = 0
|
||||
|
||||
private static readonly SELECTORS = {
|
||||
passkeySecondary: 'button[data-testid="secondaryButton"]',
|
||||
passkeyPrimary: 'button[data-testid="primaryButton"]',
|
||||
passkeyTitle: '[data-testid="title"]',
|
||||
kmsiVideo: '[data-testid="kmsiVideo"]',
|
||||
biometricVideo: '[data-testid="biometricVideo"]'
|
||||
} as const
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
public async disableFido(page: Page) {
|
||||
await page.route('**/GetCredentialType.srf*', route => {
|
||||
try {
|
||||
const body = JSON.parse(route.request().postData() || '{}')
|
||||
body.isFidoSupported = false
|
||||
route.continue({ postData: JSON.stringify(body) })
|
||||
} catch { /* Route continue on parse failure */ route.continue() }
|
||||
}).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile))
|
||||
}
|
||||
|
||||
public async handlePasskeyPrompts(page: Page, context: 'main' | 'oauth') {
|
||||
let did = false
|
||||
|
||||
// Priority 1: Direct detection of "Skip for now" button by data-testid
|
||||
const skipBtnResult = await waitForElementSmart(page, 'button[data-testid="secondaryButton"]', {
|
||||
initialTimeoutMs: 300,
|
||||
extendedTimeoutMs: 500,
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
if (skipBtnResult.found && skipBtnResult.element) {
|
||||
const text = (await skipBtnResult.element.textContent() || '').trim()
|
||||
// Check if it's actually a skip button (could be other secondary buttons)
|
||||
if (/skip|later|not now|non merci|pas maintenant/i.test(text)) {
|
||||
await skipBtnResult.element.click().catch(logError('LOGIN-PASSKEY', 'Skip button click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('data-testid secondaryButton')
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Video heuristic (biometric prompt)
|
||||
if (!did) {
|
||||
const biometricResult = await waitForElementSmart(page, PasskeyHandler.SELECTORS.biometricVideo, {
|
||||
initialTimeoutMs: 300,
|
||||
extendedTimeoutMs: 500,
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
if (biometricResult.found) {
|
||||
const btnResult = await waitForElementSmart(page, PasskeyHandler.SELECTORS.passkeySecondary, {
|
||||
initialTimeoutMs: 200,
|
||||
extendedTimeoutMs: 300,
|
||||
state: 'visible'
|
||||
})
|
||||
if (btnResult.found && btnResult.element) {
|
||||
await btnResult.element.click().catch(logError('LOGIN-PASSKEY', 'Video heuristic click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('video heuristic')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Title + secondary button detection
|
||||
if (!did) {
|
||||
const titleResult = await waitForElementSmart(page, PasskeyHandler.SELECTORS.passkeyTitle, {
|
||||
initialTimeoutMs: 300,
|
||||
extendedTimeoutMs: 500,
|
||||
state: 'attached'
|
||||
})
|
||||
|
||||
if (titleResult.found && titleResult.element) {
|
||||
const title = (await titleResult.element.textContent() || '').trim()
|
||||
const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title)
|
||||
|
||||
if (looksLike) {
|
||||
const secBtnResult = await waitForElementSmart(page, PasskeyHandler.SELECTORS.passkeySecondary, {
|
||||
initialTimeoutMs: 200,
|
||||
extendedTimeoutMs: 300,
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
if (secBtnResult.found && secBtnResult.element) {
|
||||
await secBtnResult.element.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('title heuristic ' + title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check secondary button text if title heuristic didn't work
|
||||
if (!did) {
|
||||
const secBtnResult = await waitForElementSmart(page, PasskeyHandler.SELECTORS.passkeySecondary, {
|
||||
initialTimeoutMs: 200,
|
||||
extendedTimeoutMs: 300,
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
if (secBtnResult.found && secBtnResult.element) {
|
||||
const text = (await secBtnResult.element.textContent() || '').trim()
|
||||
if (/skip for now|not now|later|passer|plus tard/i.test(text)) {
|
||||
await secBtnResult.element.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('secondary button text')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: XPath fallback (includes Windows Hello specific patterns)
|
||||
if (!did) {
|
||||
const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now") or contains(normalize-space(.),"Not now") or contains(normalize-space(.),"Passer") or contains(normalize-space(.),"No thanks")]').first()
|
||||
// FIXED: Add explicit timeout to isVisible
|
||||
if (await textBtn.isVisible({ timeout: 500 }).catch(() => false)) {
|
||||
await textBtn.click().catch(logError('LOGIN-PASSKEY', 'XPath fallback click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('xpath fallback')
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4.5: Windows Hello specific detection
|
||||
if (!did) {
|
||||
// FIXED: Add explicit timeout
|
||||
const windowsHelloTitle = await page.locator('text=/windows hello/i').first().isVisible({ timeout: 500 }).catch(() => false)
|
||||
if (windowsHelloTitle) {
|
||||
// Try common Windows Hello skip patterns
|
||||
const skipPatterns = [
|
||||
'button:has-text("Skip")',
|
||||
'button:has-text("No thanks")',
|
||||
'button:has-text("Maybe later")',
|
||||
'button:has-text("Cancel")',
|
||||
'[data-testid="secondaryButton"]',
|
||||
'button[class*="secondary"]'
|
||||
]
|
||||
for (const pattern of skipPatterns) {
|
||||
const btn = await page.locator(pattern).first()
|
||||
// FIXED: Add explicit timeout
|
||||
if (await btn.isVisible({ timeout: 300 }).catch(() => false)) {
|
||||
await btn.click().catch(logError('LOGIN-PASSKEY', 'Windows Hello skip failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('Windows Hello skip')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 5: Close button fallback (FIXED: Add explicit timeout instead of using page.$)
|
||||
if (!did) {
|
||||
const closeResult = await waitForElementSmart(page, '#close-button', {
|
||||
initialTimeoutMs: 300,
|
||||
extendedTimeoutMs: 500,
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
if (closeResult.found && closeResult.element) {
|
||||
await closeResult.element.click().catch(logError('LOGIN-PASSKEY', 'Close button fallback failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('close button')
|
||||
}
|
||||
}
|
||||
|
||||
// KMSI prompt
|
||||
const kmsi = await page.waitForSelector(PasskeyHandler.SELECTORS.kmsiVideo, { timeout: 400 }).catch(() => null)
|
||||
if (kmsi) {
|
||||
const yes = await page.$(PasskeyHandler.SELECTORS.passkeyPrimary)
|
||||
if (yes) {
|
||||
await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-KMSI', 'Accepted KMSI prompt')
|
||||
}
|
||||
}
|
||||
|
||||
if (!did && context === 'main') {
|
||||
this.noPromptIterations++
|
||||
const now = Date.now()
|
||||
if (this.noPromptIterations === 1 || now - this.lastNoPromptLog > 10000) {
|
||||
this.lastNoPromptLog = now
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-NO-PROMPT', `No dialogs (x${this.noPromptIterations})`)
|
||||
if (this.noPromptIterations > 50) this.noPromptIterations = 0
|
||||
}
|
||||
} else if (did) {
|
||||
this.noPromptIterations = 0
|
||||
}
|
||||
}
|
||||
|
||||
private logPasskeyOnce(reason: string) {
|
||||
if (this.passkeyHandled) return
|
||||
this.passkeyHandled = true
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Dismissed passkey prompt (${reason})`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user