msmail / functions /utils /authService.ts
github-actions[bot]
Update from GitHub Actions
0b827be
raw
history blame
12.6 kB
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}`);
}
}