import { Page } from 'playwright'; import BrowserManager from './browser.js'; import { getVerificationCode } from './emailVerification.js'; interface Account { email: string; password: string; proofEmail: string; } export class AuthService { constructor(private env: Env) { } async authenticateEmail(email: string): Promise<{ success: boolean; error?: string }> { try { const accountsStr = await this.env.KV.get("accounts"); const accounts: Account[] = accountsStr ? JSON.parse(accountsStr) : []; const account = accounts.find(a => a.email === email); if (!account) { throw new Error("Account not found"); } await this.performAuthentication(account); await this.handleAuthorizationCallback(email); return { success: true }; } catch (error: any) { return { success: false, error: error.message }; } } private async performAuthentication(account: Account) { const clientId = this.env.ENTRA_CLIENT_ID; const redirectUri = this.env.AUTH_REDIRECT_URI; let browser; let context; try { browser = await BrowserManager.getInstance(); context = await browser.newContext(); const page = await context.newPage(); const authUrl = this.buildAuthUrl(clientId, redirectUri, account.email); await this.handleLoginProcess(page, account, authUrl); await this.handleMultiFactorAuth(page, account); await this.confirmLogin(page, account); await this.handleConsent(page, redirectUri); } finally { if (context) await context.close(); } } private buildAuthUrl(clientId: string, redirectUri: string, email: string): string { return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + `client_id=${clientId}` + `&response_type=code` + `&redirect_uri=${encodeURIComponent(redirectUri)}` + `&response_mode=query` + `&scope=offline_access%20IMAP.AccessAsUser.All%20User.Read%20Mail.ReadWrite.Shared%20Mail.Send%20Mail.Read` + `&prompt=consent` + `&state=${email}`; } private async handleLoginProcess(page: Page, account: Account, authUrl: string) { await page.goto(authUrl); await page.fill('input[type="email"]', account.email); await page.click('input[type="submit"]'); try { await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 3000 }); await page.click('#idA_PWD_SwitchToPassword'); } catch (error) { console.log(account.email, `没有旧版切换到密码登录,继续执行: ${error}`); } try { const passwordButtonByRole = page.getByRole('button', { name: '使用密码' }); if (await passwordButtonByRole.isVisible({ timeout: 3000 })) { await passwordButtonByRole.click(); } } catch (error) { console.log(account.email, `没有新版切换到密码登录,继续执行: ${error}`); } await page.waitForURL("https://login.live.com/**"); await page.fill('input[type="password"]', account.password); await page.fill('input[type="password"]', account.password); await page.click('button[type="submit"]'); const proofEmail = account.proofEmail; try { await page.waitForURL((url) => { return url.href.startsWith('https://account.live.com/recover'); }, { timeout: 3000 }); await page.click('input[type="submit"]#iLandingViewAction'); const timestamp = Math.floor(Date.now() / 1000); await page.fill("#iProofEmail", proofEmail) await page.click('input[type="submit"]') const proofConfig = this.getProofConfig(proofEmail); const verificationCode = await getVerificationCode(proofConfig.apiUrl, proofConfig.token!, proofEmail, timestamp); await page.fill('input[type="tel"]', verificationCode); await page.click('input[type="submit"]'); //可能需要修改密码..这里就不处理了 } catch (error) { console.log(account.email, `没有帮助我们保护帐户确认,继续执行: ${error}`); } try { //新版的邮箱验证 await page.waitForURL((url) => { return url.href.startsWith('https://login.live.com/oauth20_authorize.srf'); }, { timeout: 3000 }); const timestamp = Math.floor(Date.now() / 1000); await page.fill("#proof-confirmation-email-input", proofEmail) await page.click('button[type="submit"]') const proofConfig = this.getProofConfig(proofEmail); const verificationCode = await getVerificationCode(proofConfig.apiUrl, proofConfig.token!, proofEmail, timestamp); await page.fill('input#codeEntry-0', verificationCode[0]); await page.fill('input#codeEntry-1', verificationCode[1]); await page.fill('input#codeEntry-2', verificationCode[2]); await page.fill('input#codeEntry-3', verificationCode[3]); await page.fill('input#codeEntry-4', verificationCode[4]); await page.fill('input#codeEntry-5', verificationCode[5]); //可能需要修改密码..这里就不处理了 } catch (error) { console.log(account.email, `没有帮助我们保护帐户确认,继续执行: ${error}`); } } private getProofConfig(proofEmail: string) { const proof = [ { "suffix": "godgodgame.com", "apiUrl": "https://seedmail.igiven.com/api/latest-email", "token": this.env.PROOF_GODGODGAME_TOKEN }, { "suffix": "igiven.com", "apiUrl": "https://mail.igiven.com/api/latest-email", "token": this.env.PROOF_IGIVEN_TOKEN } ]; const suffix = proofEmail.substring(proofEmail.indexOf('@') + 1); const proofConfig = proof.find(p => p.suffix === suffix)!; return proofConfig; } private async handleMultiFactorAuth(page: Page, account: Account) { for (let i = 0; i < 3; i++) { try { await page.waitForURL('https://account.live.com/identity/**', { timeout: 5000 }); const proofEmail = account.proofEmail; if (!proofEmail) { throw new Error("No proof email provided"); } const timestamp = Math.floor(Date.now() / 1000); try { await page.waitForSelector('#iProof0', { timeout: 3000 }); await page.click('#iProof0'); } catch (error) { console.log(`没有#iProof0,继续执行: ${error}`); } await page.fill("#iProofEmail", proofEmail); await page.click('input[type="submit"]'); const proofConfig = this.getProofConfig(proofEmail); const verificationCode = await getVerificationCode( proofConfig.apiUrl, proofConfig.token!, proofEmail, timestamp ); await page.fill('input[type="tel"]', verificationCode); await page.click('input[type="submit"]'); await page.waitForTimeout(5 * 1000) } catch (error) { console.log(account.email, `没有多重验证,继续执行: ${error}`); } } } async confirmLogin(page: Page, account: Account) { try { await page.waitForURL('https://account.live.com/interrupt/**', { timeout: 3000 }); // 尝试查找"暂时跳过"按钮 const skipButtonExists = await page.isVisible('button[data-testid="secondaryButton"]', { timeout: 5000 }); if (skipButtonExists) { console.log(account.email, "找到新版'暂时跳过'按钮,正在点击..."); await page.click('button[data-testid="secondaryButton"]'); } // 最后尝试你提到的另一个按钮 const otherButtonExists = await page.isVisible('div[data-testid="textButtonContainer"] > div:first-child > button[type="button"]', { timeout: 5000 }); if (otherButtonExists) { console.log(account.email, "找到旧版'暂时跳过'按钮,正在点击..."); await page.click('div[data-testid="textButtonContainer"] > div:first-child > button[type="button"]'); } } catch (error) { //暂时跳过.下一个.获取微软的通行密钥软件 console.log(account.email, `无获取通行密钥提示,继续执行: ${error}`); } try { //和下面一样.随机出现 await page.waitForURL('https://login.live.com/ppsecure/**', { timeout: 3000 }); //新版可能有问题.换成如下. //await page.click('#acceptButton', { timeout: 10000 }); //await page.locator('button[type="submit"]').nth(0).click({ timeout: 10000 }); await page.locator('button[type="submit"]').first().click({ timeout: 10000 }); } catch (error) { console.log(account.email, `无ppsecure登录确认,继续执行: ${error}`); } try { //和上面一样.随机出现 await page.waitForURL((url) => { return url.href.startsWith('https://login.live.com/oauth20_authorize.srf'); }, { timeout: 3000 }); await page.click('button[type="submit"]', { timeout: 10000 }); } catch (error) { console.log(account.email, `无oauth20_authorize登录确认,继续执行: ${error}`); } try { //旧版的登录确认 await page.waitForURL((url) => { return url.href.startsWith('https://login.live.com'); }, { timeout: 3000 }); await page.click('button[type="submit"]#acceptButton',{timeout: 3000}); } catch (error) { console.log(account.email, `无其他登录确认,继续执行: ${error}`); } } private async handleConsent(page: Page, redirectUri: string) { try { await page.waitForURL("https://account.live.com/Consent/**", { timeout: 10000 }); await page.click('button[type="submit"][data-testid="appConsentPrimaryButton"]'); } catch (error) { console.log("Consent page not found or timeout, skipping..."); } await page.waitForURL((url: any) => url.href.startsWith(redirectUri)); } private async handleAuthorizationCallback(email: string) { let code = null; const maxRetries = 30; let retries = 0; while (!code && retries < maxRetries) { const codeKey = `code_${email}`; code = await this.env.KV.get(codeKey); if (!code) { await new Promise(resolve => setTimeout(resolve, 1000)); retries++; } } if (!code) { throw new Error("Authorization timeout"); } const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: this.env.ENTRA_CLIENT_ID, client_secret: this.env.ENTRA_CLIENT_SECRET, code: code, redirect_uri: this.env.AUTH_REDIRECT_URI, grant_type: 'authorization_code' }) }); const tokenData: any = await tokenResponse.json(); if (!tokenData.refresh_token) { throw new Error("Failed to get refresh token"); } if (!tokenData.expires_in) { throw new Error("Missing expires_in in token response"); } const tokenInfo = { ...tokenData, timestamp: Date.now() }; await this.env.KV.put(`refresh_token_${email}`, JSON.stringify(tokenInfo)); await this.env.KV.delete(`code_${email}`); } }