|
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.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(`没有切换到密码登录,继续执行: ${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"]'); |
|
|
|
try { |
|
await page.waitForURL("https://account.live.com/recover/**", { timeout: 5000 }); |
|
await page.click('#iLandingViewAction'); |
|
} catch (error) { |
|
console.log("验证确定 page not found or timeout, skipping..."); |
|
} |
|
} |
|
|
|
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 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)!; |
|
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}`); |
|
} |
|
} |
|
} |
|
|
|
private async handleConsent(page: Page, redirectUri: string) { |
|
await page.waitForURL((url: any) => url.href.startsWith("https://login.live.com")); |
|
await page.click('button[type="submit"]#acceptButton'); |
|
|
|
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}`); |
|
} |
|
} |