Files
Microsoft-Rewards-Script/src/functions/Login.ts
2025-08-10 15:10:38 +02:00

406 lines
17 KiB
TypeScript

import { Page } from 'rebrowser-playwright'
import readline from 'readline'
import * as crypto from 'crypto'
import { AxiosRequestConfig } from 'axios'
import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load'
import { OAuth } from '../interface/OAuth'
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
export class Login {
private bot: MicrosoftRewardsBot
private clientId: string = '0000000040170455'
private authBaseUrl: string = 'https://login.live.com/oauth20_authorize.srf'
private redirectUrl: string = 'https://login.live.com/oauth20_desktop.srf'
private tokenUrl: string = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
private scope: string = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async login(page: Page, email: string, password: string) {
try {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Starting login process!')
// Navigate to the Bing login page
await page.goto('https://rewards.bing.com/signin')
await page.waitForLoadState('domcontentloaded').catch(() => { })
await this.bot.browser.utils.reloadBadPage(page)
// Check if account is locked
await this.checkAccountLocked(page)
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 }).then(() => true).catch(() => false)
if (!isLoggedIn) {
await this.execLogin(page, email, password)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in')
// Check if account is locked
await this.checkAccountLocked(page)
}
// Check if logged in to bing
await this.checkBingLogin(page)
// Save session
await saveSessionData(this.bot.config.sessionPath, page.context(), email, this.bot.isMobile)
// We're done logging in
this.bot.log(this.bot.isMobile, 'LOGIN', 'Logged in successfully, saved login session!')
} catch (error) {
// Throw and don't continue
throw this.bot.log(this.bot.isMobile, 'LOGIN', 'An error occurred:' + error, 'error')
}
}
private async execLogin(page: Page, email: string, password: string) {
try {
await this.enterEmail(page, email)
await this.bot.utils.wait(2000)
await this.bot.browser.utils.reloadBadPage(page)
await this.bot.utils.wait(2000)
await this.enterPassword(page, password)
await this.bot.utils.wait(2000)
// Check if account is locked
await this.checkAccountLocked(page)
await this.bot.browser.utils.reloadBadPage(page)
await this.checkLoggedIn(page)
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'An error occurred: ' + error, 'error')
}
}
private async enterEmail(page: Page, email: string) {
const emailInputSelector = 'input[type="email"]'
try {
// Wait for email field
const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 2000 }).catch(() => null)
if (!emailField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found', 'warn')
return
}
await this.bot.utils.wait(1000)
// Check if email is prefilled
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 5000 }).catch(() => null)
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
} else {
// Else clear and fill email
await page.fill(emailInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(emailInputSelector, email)
await this.bot.utils.wait(1000)
}
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after email entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Email entry failed: ${error}`, 'error')
}
}
private async enterPassword(page: Page, password: string) {
const passwordInputSelector = 'input[type="password"]'
const skip2FASelector = '#idA_PWD_SwitchToPassword';
try {
const skip2FAButton = await page.waitForSelector(skip2FASelector, { timeout: 2000 }).catch(() => null)
if (skip2FAButton) {
await skip2FAButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Skipped 2FA')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'No 2FA skip button found, proceeding with password entry')
}
const viewFooter = await page.waitForSelector('#view > div > span:nth-child(6)', { timeout: 2000 }).catch(() => null)
const passwordField1 = await page.waitForSelector(passwordInputSelector, { timeout: 5000 }).catch(() => null)
if (viewFooter && !passwordField1) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page "Get a code to sign in" found by "viewFooter"')
const otherWaysButton = await viewFooter.$('span[role="button"]')
if (otherWaysButton) {
await otherWaysButton.click()
await this.bot.utils.wait(5000)
const secondListItem = page.locator('[role="listitem"]').nth(1)
if (await secondListItem.isVisible()) {
await secondListItem.click()
}
}
}
// Wait for password field
const passwordField = await page.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
if (!passwordField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field not found, possibly 2FA required', 'warn')
await this.handle2FA(page)
return
}
await this.bot.utils.wait(1000)
// Clear and fill password
await page.fill(passwordInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(passwordInputSelector, password)
await this.bot.utils.wait(1000)
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after password entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry failed: ${error}`, 'error')
await this.handle2FA(page)
}
}
private async handle2FA(page: Page) {
try {
const numberToPress = await this.get2FACode(page)
if (numberToPress) {
// Authentictor App verification
await this.authAppVerification(page, numberToPress)
} else {
// SMS verification
await this.authSMSVerification(page)
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `2FA handling failed: ${error}`)
}
}
private async get2FACode(page: Page): Promise<string | null> {
try {
const element = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { state: 'visible', timeout: 2000 })
return await element.textContent()
} catch {
if (this.bot.config.parallel) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Script running in parallel, can only send 1 2FA request per account at a time!', 'log', 'yellow')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Trying again in 60 seconds! Please wait...', 'log', 'yellow')
// eslint-disable-next-line no-constant-condition
while (true) {
const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null)
if (button) {
await this.bot.utils.wait(60000)
await button.click()
continue
} else {
break
}
}
}
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
await this.bot.utils.wait(2000)
const element = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { state: 'visible', timeout: 2000 })
return await element.textContent()
}
}
private async authAppVerification(page: Page, numberToPress: string | null) {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
this.bot.log(this.bot.isMobile, 'LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`)
this.bot.log(this.bot.isMobile, 'LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds')
await page.waitForSelector('form[name="f1"]', { state: 'detached', timeout: 60000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!')
break
} catch {
this.bot.log(this.bot.isMobile, 'LOGIN', 'The code is expired. Trying to get a new code...')
await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]')
numberToPress = await this.get2FACode(page)
}
}
}
private async authSMSVerification(page: Page) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'SMS 2FA code required. Waiting for user input...')
const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => {
rl.close()
resolve(input)
})
})
await page.fill('input[name="otc"]', code)
await page.keyboard.press('Enter')
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code entered successfully')
}
async getMobileAccessToken(page: Page, email: string) {
const authorizeUrl = new URL(this.authBaseUrl)
authorizeUrl.searchParams.append('response_type', 'code')
authorizeUrl.searchParams.append('client_id', this.clientId)
authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl)
authorizeUrl.searchParams.append('scope', this.scope)
authorizeUrl.searchParams.append('state', crypto.randomBytes(16).toString('hex'))
authorizeUrl.searchParams.append('access_type', 'offline_access')
authorizeUrl.searchParams.append('login_hint', email)
await page.goto(authorizeUrl.href)
let currentUrl = new URL(page.url())
let code: string
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Waiting for authorization...')
// eslint-disable-next-line no-constant-condition
while (true) {
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
code = currentUrl.searchParams.get('code')!
break
}
currentUrl = new URL(page.url())
await this.bot.utils.wait(5000)
}
const body = new URLSearchParams()
body.append('grant_type', 'authorization_code')
body.append('client_id', this.clientId)
body.append('code', code)
body.append('redirect_uri', this.redirectUrl)
const tokenRequest: AxiosRequestConfig = {
url: this.tokenUrl,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: body.toString()
}
const tokenResponse = await this.bot.axios.request(tokenRequest)
const tokenData: OAuth = await tokenResponse.data
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Successfully authorized')
return tokenData.access_token
}
// Utils
private async checkLoggedIn(page: Page) {
const targetHostname = 'rewards.bing.com'
const targetPathname = '/'
// eslint-disable-next-line no-constant-condition
while (true) {
await this.dismissLoginMessages(page)
const currentURL = new URL(page.url())
if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) {
break
}
}
// Wait for login to complete
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal')
}
private async dismissLoginMessages(page: Page) {
// Use Passekey
if (await page.waitForSelector('[data-testid="biometricVideo"]', { timeout: 2000 }).catch(() => null)) {
const skipButton = await page.$('[data-testid="secondaryButton"]')
if (skipButton) {
await skipButton.click()
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-LOGIN-MESSAGES', 'Dismissed "Use Passekey" modal')
await page.waitForTimeout(500)
}
}
// Use Keep me signed in
if (await page.waitForSelector('[data-testid="kmsiVideo"]', { timeout: 2000 }).catch(() => null)) {
const yesButton = await page.$('[data-testid="primaryButton"]')
if (yesButton) {
await yesButton.click()
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-LOGIN-MESSAGES', 'Dismissed "Keep me signed in" modal')
await page.waitForTimeout(500)
}
}
}
private async checkBingLogin(page: Page): Promise<void> {
try {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing login')
await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F')
const maxIterations = 5
for (let iteration = 1; iteration <= maxIterations; iteration++) {
const currentUrl = new URL(page.url())
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
await this.bot.browser.utils.tryDismissAllMessages(page)
const loggedIn = await this.checkBingLoginStatus(page)
// If mobile browser, skip this step
if (loggedIn || this.bot.isMobile) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing login verification passed!')
break
}
}
await this.bot.utils.wait(1000)
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'An error occurred:' + error, 'error')
}
}
private async checkBingLoginStatus(page: Page): Promise<boolean> {
try {
await page.waitForSelector('#id_n', { timeout: 5000 })
return true
} catch (error) {
return false
}
}
private async checkAccountLocked(page: Page) {
await this.bot.utils.wait(2000)
const isLocked = await page.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 1000 }).then(() => true).catch(() => false)
if (isLocked) {
throw this.bot.log(this.bot.isMobile, 'CHECK-LOCKED', 'This account has been locked! Remove the account from "accounts.json" and restart!', 'error')
}
}
}