msmail / functions /utils /authService.ts
github-actions[bot]
Update from GitHub Actions
1877b95
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}`);
}
}