GitHub MFA Automation
Giới thiệu
Thách thức lớn nhất trong việc tự động hóa đăng nhập GitHub là Xác thực Hai yếu tố (2FA). Dù là Ứng dụng Authenticator (TOTP) hay Email OTP, quy trình tự động hóa truyền thống thường bị dừng lại ở bước này do:
- Không thể tự động lấy mã xác thực
- Không đồng bộ mã thời gian thực
- Không thể tự động nhập mã thông qua tự động hóa
- Môi trường trình duyệt không đủ thực tế, kích hoạt kiểm tra bảo mật của GitHub
Bài viết này trình bày cách xây dựng quy trình 2FA GitHub tự động hoàn toàn bằng Scrapeless Browser + Signal CDP, bao gồm:
- Trường hợp 1: GitHub 2FA (Chế độ Authenticator / TOTP tự sinh)
- Trường hợp 2: GitHub 2FA (Nghe Email OTP tự động)
Chúng tôi sẽ giải thích toàn bộ quy trình cho từng trường hợp và trình bày cách phối hợp script đăng nhập với bộ lắng nghe mã xác thực trong hệ thống tự động.
Trường hợp 1: GitHub 2FA (Chế độ Authenticator OTP)
Cơ chế TOTP (Mã dùng một lần theo thời gian) của GitHub rất phù hợp với các kịch bản tự động hóa. Với Scrapeless Browser + Signal CDP, trình duyệt có thể tự động:
- Kích hoạt sự kiện khi đến trang 2FA
- Sinh mã OTP
- Tự động điền mã
- Hoàn tất đăng nhập
So với OTP Email/SMS truyền thống, TOTP có ưu điểm:
- Mã sinh tại chỗ, không phụ thuộc bên ngoài
- Sinh mã nhanh và ổn định
- Không cần API bổ sung
- Hoàn toàn tự động mà không cần can thiệp người dùng
Các kịch bản áp dụng:
- Tài khoản GitHub dùng Google Authenticator / Authy / 1Password
- Đường dẫn trang 2FA:
/sessions/two-factor/apphoặc/sessions/two-factor/webauthn
Video

Bước 1: Kết nối tới Scrapeless Browser
Ở bước này, ta thiết lập kết nối WebSocket hai chiều với Scrapeless Browser:
import puppeteer from 'puppeteer-core';
const query = new URLSearchParams({
token: "",
proxyCountry: "ANY",
sessionRecording: true,
sessionTTL: 900,
sessionName: "Data Scraping",
});
const connectionURL = `wss://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
const browserWSEndpoint = connectionURL;
const browser = await puppeteer.connect({ browserWSEndpoint });
console.log("✅ Đã kết nối tới Scrapeless Browser");Ưu điểm:
- Chrome thực trên cloud với khả năng chống phát hiện mạnh mẽ
- Không tiêu tốn tài nguyên cục bộ
- Tự động thiết lập proxy, lưu phiên và ghi lại phiên làm việc
- Thực thi ổn định các quy trình đăng nhập tự động quy mô lớn
Bước 2: Khởi tạo MFA Manager + Nhà cung cấp TOTP
Xác thực MFA được xử lý qua Signal CDP giao tiếp hai chiều, bao gồm:
- Tự động gửi
mfa_code_requestkhi phát hiện trang 2FA - Sinh mã TOTP
- Trả mã qua
mfa_code_response - Gửi các sự kiện kết quả đăng nhập
import { authenticator } from 'otplib';
const TOTP_SECRETS = {
'github': 'secret-code',
'default': 'secret-code'
};
authenticator.options = {
digits: 6,
step: 30,
window: 1
};Bước 3: Tạo MFA Manager
Tại đây, chúng ta tạo bộ quản lý MFA tập trung để xử lý giao tiếp với Signal CDP:
- Kích hoạt
mfa_code_request - Chờ và nhận mã xác thực (
mfa_code_response) - Trả mã về quy trình đăng nhập GitHub
- Gửi sự kiện kết quả đăng nhập cuối cùng
class MFAManager {
constructor() {
this.client = null;
}
setClient(client) {
this.client = client;
}
async sendMFARequest(username, provider = 'github') {
const requestData = {
type: 'mfa_required',
provider,
username,
timestamp: new Date().toISOString(),
service: 'github_2fa'
};
return await this.client.send('Signal.send', {
event: 'mfa_code_request',
data: JSON.stringify(requestData)
});
}
async waitForMFACode(timeout = 120000) {
const result = await this.client.send('Signal.wait', {
event: 'mfa_code_response',
timeout
});
if (result.status === 200 && result.data) {
return JSON.parse(result.data).code;
} else if (result.status === 408) {
throw new Error('Chờ mã MFA quá thời gian cho phép');
} else {
throw new Error(`Signal wait thất bại, trạng thái: ${result.status}`);
}
}
async sendMFAResponse(code, username) {
const responseData = {
code,
username,
timestamp: new Date().toISOString(),
status: 'code_provided'
};
return await this.client.send('Signal.send', {
event: 'mfa_code_response',
data: JSON.stringify(responseData)
});
}
async sendLoginResult(success, username, url, error = null) {
const resultData = {
success,
username,
url,
timestamp: new Date().toISOString(),
error
};
await this.client.send('Signal.send', {
event: 'github_login_result',
data: JSON.stringify(resultData)
});
}
}Ưu điểm:
- Quy trình 2FA tự động hoàn toàn (yêu cầu → chờ → nhận → điền)
- Quản lý tập trung đa phương thức xác thực (TOTP/Email/Push)
- Loại bỏ trùng lặp logic 2FA trong scripts
Bước 4: Khởi chạy Nhà cung cấp Mã TOTP (Xử lý nghe & tự động điền mã)
Bước này chạy bộ lắng nghe mã TOTP tự động, thực hiện:
- Liên tục nghe sự kiện
mfa_code_request - Tự động sinh mã TOTP (dùng
otplib) - Tự động gửi lại mã qua Signal (
mfa_code_response) - Thay thế hoàn toàn việc nhập mã thủ công
class TOTPCodeProvider {
constructor(mfaManager) {
this.mfaManager = mfaManager;
this.isListening = false;
this.secrets = TOTP_SECRETS;
}
async startListening() {
if (this.isListening) return;
this.isListening = true;
this.listenLoop();
}
async listenLoop() {
while (this.isListening) {
try {
const result = await this.mfaManager.client.send('Signal.wait', {
event: 'mfa_code_request',
timeout: 5000
});
if (result.status === 200 && result.data) {
const requestData = JSON.parse(result.data);
await this.handleMFARequest(requestData);
}
} catch (err) {}
await new Promise(r => setTimeout(r, 100));
}
}
async handleMFARequest(requestData) {
const code = this.generateTOTPCode(requestData.provider);
await this.mfaManager.sendMFAResponse(code, requestData.username);
console.log(`✅ Mã TOTP đã gửi qua Signal CDP: ${code}`);
}
generateTOTPCode(service = 'github') {
const secret = this.secrets[service] || this.secrets.default;
return authenticator.generate(secret);
}
}Ưu điểm:
- Tự động hóa hoàn toàn 2FA không cần nhập tay
- Không cần email hay ứng dụng di động
- Sinh mã TOTP chính xác, ổn định, dễ mở rộng
- Hỗ trợ GitHub, AWS, Azure, Google, và bất kỳ nhà cung cấp TOTP nào
Bước 5: Truy cập trang đăng nhập GitHub và Nhập thông tin
Bước này xử lý phần đầu luồng đăng nhập GitHub:
Mở trang đăng nhập → Nhập tên đăng nhập & mật khẩu → Tiến đến bước kế (2FA hoặc đăng nhập trực tiếp)
await page.goto('https://github.com/login', { waitUntil: 'networkidle2' });
await page.waitForSelector('#login_field', { timeout: 10000 });
await page.type('#login_field', githubCredentials.username);
await page.type('#password', githubCredentials.password);
await page.click('input[type="submit"][value="Sign in"]');Ưu điểm:
- Dùng Scrapeless Browser đảm bảo môi trường trình duyệt chân thực, ổn định
- Gõ tự động mô phỏng hành vi người dùng thực, tránh bị cảnh báo bảo mật GitHub
- Đảm bảo điều hướng tới trang 2FA chính xác
Bước 6: Phát hiện trang 2FA và Tự động gửi TOTP
if (currentUrl.includes('/sessions/two-factor')) {
console.log('🔐 Phát hiện yêu cầu 2FA');
await mfaManager.sendMFARequest(githubCredentials.username, 'github');
const mfaCode = await mfaManager.waitForMFACode(120000);
await page.waitForSelector('#app_totp');
await page.type('#app_totp', mfaCode);
await new Promise(resolve => setTimeout(resolve, 5000));
}Ưu điểm:
- Hoàn tất 2FA GitHub mà không cần can thiệp người dùng
- Mã TOTP tự sinh, tránh trễ do mã email
- Giao tiếp hai chiều Signal CDP đảm bảo chuyển mã chính xác
- Độ ổn định và tin cậy tầm doanh nghiệp
Bước 7: Gửi kết quả đăng nhập qua Signal CDP
Sau khi đăng nhập + 2FA hoàn tất, kết quả được gửi qua CDP (Signal.send) tới hệ thống tự động. Backend, bot hoặc hệ thống CI/CD có thể nhận biết kịp thời trạng thái đăng nhập.
const isLoggedIn = !finalUrl.includes('/sessions/two-factor')
&& !finalUrl.includes('/login');
await mfaManager.sendLoginResult(isLoggedIn, githubCredentials.username, finalUrl);Mã hoàn chỉnh
import puppeteer from 'puppeteer-core';
import {authenticator} from 'otplib';
const query = new URLSearchParams({
token: "api-key",
proxyCountry: "ANY",
sessionRecording: true,
sessionTTL: 900,
sessionName: "GithubLogin",
});
const browserWSEndpoint = `wss://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
// TOTP secrets
const TOTP_SECRETS = {
'github': 'secret-code',
'default': 'secret-code'
};
// Configure TOTP
authenticator.options = {digits: 6, step: 30, window: 1};
// MFA Manager
class MFAManager {
constructor() {
this.client = null;
}
setClient(client) {
this.client = client;
}
async sendMFARequest(username, provider = 'github') {
return await this.client.send('Signal.send', {
event: 'mfa_code_request',
data: JSON.stringify({
type: 'mfa_required',
provider,
username,
timestamp: new Date().toISOString(),
service: 'github_2fa'
})
});
}
async waitForMFACode(timeout = 120000) {
const result = await this.client.send('Signal.wait', {
event: 'mfa_code_response',
timeout
});
if (result.status === 200 && result.data) {
return JSON.parse(result.data).code;
} else if (result.status === 408) {
throw new Error('Timeout waiting for MFA code');
} else {
throw new Error(`Signal wait failed, status: ${result.status}`);
}
}
async sendMFAResponse(code, username) {
return await this.client.send('Signal.send', {
event: 'mfa_code_response',
data: JSON.stringify({code, username, timestamp: new Date().toISOString(), status: 'code_provided'})
});
}
async sendLoginResult(success, username, url, error = null) {
await this.client.send('Signal.send', {
event: 'github_login_result',
data: JSON.stringify({success, username, url, timestamp: new Date().toISOString(), error})
});
}
}
// TOTP provider
class TOTPCodeProvider {
constructor(mfaManager) {
this.mfaManager = mfaManager;
this.isListening = false;
this.secrets = TOTP_SECRETS;
}
async startListening() {
if (this.isListening) return;
this.isListening = true;
this.listenLoop();
}
async listenLoop() {
while (this.isListening) {
try {
const result = await this.mfaManager.client.send('Signal.wait', {
event: 'mfa_code_request',
timeout: 5000
});
if (result.status === 200 && result.data) {
const requestData = JSON.parse(result.data);
await this.handleMFARequest(requestData);
}
} catch (error) {
}
await new Promise(resolve => setTimeout(resolve, 100));
}
}
async handleMFARequest(requestData) {
try {
const code = this.generateTOTPCode(requestData.provider);
if (code) {
await this.mfaManager.sendMFAResponse(code, requestData.username);
console.log(`✅ Mã TOTP đã gửi qua Signal CDP: ${code}`);
} else {
await this.sendErrorResponse(requestData.username, 'Không thể sinh mã TOTP');
}
} catch (error) {
await this.sendErrorResponse(requestData.username, error.message);
}
}
generateTOTPCode(service = 'github') {
const secret = this.getSecretForService(service);
if (!secret) throw new Error(`Không tìm thấy secret TOTP cho ${service}`);
const token = authenticator.generate(secret);
if (!token || token.length !== 6 || isNaN(token)) {
throw new Error(`Mã TOTP không hợp lệ sinh ra: ${token}`);
}
return token;
}
getSecretForService(service) {
if (this.secrets[service]) return this.secrets[service];
const lower = service.toLowerCase();
if (this.secrets[lower]) return this.secrets[lower];
if (this.secrets.default) return this.secrets.default;
return null;
}
async sendErrorResponse(username, errorMessage) {
await this.mfaManager.client.send('Signal.send', {
event: 'mfa_code_error',
data: JSON.stringify({username, error: errorMessage, timestamp: new Date().toISOString()})
});
}
stopListening() {
this.isListening = false;
}
}
// Thông tin đăng nhập GitHub
const githubCredentials = {
username: "***@gmail.com",
password: "****"
};
// Luồng đăng nhập chính
async function githubLoginWithAutoMFA() {
let browser, page, codeProvider;
try {
console.log("🚀 Bắt đầu luồng đăng nhập GitHub...");
browser = await puppeteer.connect({browserWSEndpoint});
const pages = await browser.pages();
page = pages.length > 0 ? pages[0] : await browser.newPage();
const client = await page.target().createCDPSession();
const mfaManager = new MFAManager();
mfaManager.setClient(client);
codeProvider = new TOTPCodeProvider(mfaManager);
await codeProvider.startListening();
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
await page.goto('https://github.com/login', {waitUntil: 'networkidle2'});
await page.waitForSelector('#login_field, input[name="login"]', {timeout: 10000});
await page.type('#login_field', githubCredentials.username);
await page.type('#password', githubCredentials.password);
await page.click('input[type="submit"][value="Sign in"]');
await new Promise(resolve => setTimeout(resolve, 3000));
const currentUrl = page.url();
if (currentUrl.includes('/sessions/two-factor')) {
console.log('🔐 Phát hiện yêu cầu 2FA');
await mfaManager.sendMFARequest(githubCredentials.username, 'github');
if (currentUrl.includes('/sessions/two-factor/webauthn')) {
const moreOptionsButton = await page.$('.more-options-two-factor');
if (moreOptionsButton) {
await moreOptionsButton.click();
await new Promise(resolve => setTimeout(resolve, 1000));
}
const authAppLink = await page.$('a[href="/sessions/two-factor/app"]');
if (authAppLink) {
await authAppLink.click();
await page.waitForNavigation({waitUntil: 'networkidle2'});
}
}
if (page.url().includes('/sessions/two-factor/app')) {
const mfaCode = await mfaManager.waitForMFACode(120000);
await page.waitForSelector('#app_totp', {timeout: 10000});
await page.click('#app_totp', {clickCount: 3});
await page.type('#app_totp', mfaCode);
await new Promise(resolve => setTimeout(resolve, 5000));
const finalUrl = page.url();
const isLoggedIn = !finalUrl.includes('/sessions/two-factor') &&
!finalUrl.includes('/login') &&
(finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
await mfaManager.sendLoginResult(isLoggedIn, githubCredentials.username, finalUrl);
if (isLoggedIn) {
console.log('🎉 Đăng nhập GitHub thành công!');
await page.goto('https://github.com/', {waitUntil: 'networkidle2', timeout: 10000});
} else {
console.log('❌ Đăng nhập thất bại');
}
}
} else if (currentUrl.includes('github.com') && !currentUrl.includes('/login')) {
console.log('✅ Đăng nhập thành công (không 2FA)');
await mfaManager.sendLoginResult(true, githubCredentials.username, currentUrl);
} else {
console.log('❌ Đăng nhập thất bại, vẫn ở trang đăng nhập');
await mfaManager.sendLoginResult(false, githubCredentials.username, currentUrl, 'Login failed');
}
} catch (error) {
console.error('❌ Luồng đăng nhập GitHub thất bại:', error);
try {
if (page) {
const client = await page.target().createCDPSession();
const mfaManager = new MFAManager();
mfaManager.setClient(client);
await mfaManager.sendLoginResult(false, githubCredentials.username, page.url() || 'unknown', error.message);
}
} catch (signalError) {
console.error('❌ Gửi sự kiện lỗi thất bại:', signalError);
}
} finally {
if (codeProvider) codeProvider.stopListening();
if (browser) await browser.close();
console.log('🔚 Script đăng nhập GitHub kết thúc');
}
}
githubLoginWithAutoMFA().catch(console.error);Trường hợp 2: GitHub 2FA (Chế độ Email OTP)
Loại 2FA phổ biến nhất trong môi trường doanh nghiệp là xác thực đa yếu tố (MFA) qua Email OTP.
Áp dụng cho:
- GitHub kích hoạt MFA và ràng buộc xác minh email
- Chính sách bảo mật GitHub yêu cầu xác minh Email
Video

Bước 1: Kết nối tới Scrapeless Browser
import puppeteer from 'puppeteer-core';
const query = new URLSearchParams({
token: "",
proxyCountry: "ANY",
sessionRecording: true,
sessionTTL: 900,
sessionName: "Data Scraping",
});
const connectionURL = `wss://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
const browserWSEndpoint = connectionURL;Ưu điểm:
- Thực thi trình duyệt từ xa, không tiêu hao tài nguyên cục bộ
sessionRecordinghỗ trợ phát lại và kiểm tra- Hỗ trợ giao tiếp hai chiều thời gian thực với Signal
Bước 2: Mở GitHub và nhập thông tin đăng nhập
const githubCredentials = {
username: "1040111453@qq.com",
password: "shijee1218",
twoFactorCode: null
};
const browser = await puppeteer.connect({ browserWSEndpoint });
const pages = await browser.pages();
const page = pages.length > 0 ? pages[0] : await browser.newPage();
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
await page.goto('https://github.com/login', { waitUntil: 'networkidle2' });
await page.waitForSelector('#login_field', { timeout: 10000 });Ưu điểm:
- Mô phỏng chính xác hành động người dùng thực để tránh bị bảo mật GitHub bắt lỗi
- Tự động chờ trang hiển thị, tăng độ ổn định script
- Hỗ trợ timeout dài để xử lý sự thay đổi mạng
Bước 3: Phát hiện trang 2FA Email được tải
const currentUrl = page.url();
if (currentUrl.includes('/sessions/verified-device')) {
const client = await page.target().createCDPSession();
// Gửi thông báo Signal đến bộ nghe email chuẩn bị nhận OTP
await client.send('Signal.send', {
event: 'github_2fa_required',
data: JSON.stringify({ status: '2fa_required', timestamp: new Date().toISOString() })
});
// Đợi bộ nghe email trả về OTP
const twoFactorResult = await client.send('Signal.wait', {
event: 'github_2fa_code',
timeout: 120000
});Ưu điểm:
- Phát hiện chính xác trang
/sessions/verified-device - Chủ động báo bộ nghe email chuẩn bị nhận OTP
- Hỗ trợ đợi OTP email thời gian thực, nâng cao tỷ lệ thành công tự động hóa
Bước 4: Bộ nghe Email gửi OTP
Ví dụ Signal:
{
"event": "github_2fa_code",
"data": { "code": "123456" }
}Ưu điểm:
- Tự động đọc email
- Tự động trích xuất mã OTP GitHub (6 chữ số)
- Gửi Signal về Scrapeless Browser ngay lập tức, không cần thao tác thủ công
Bước 5: Nhập OTP và gửi
if (twoFactorResult.status === 200 && twoFactorResult.data) {
const twoFactorData = JSON.parse(twoFactorResult.data);
githubCredentials.twoFactorCode = twoFactorData.code;
if (!page.url().includes('/sessions/verified-device')) return;
await page.$eval('#otp', (input) => { input.value = ''; });
await page.type('#otp', githubCredentials.twoFactorCode);
await page.evaluate(() => {
const button = document.querySelector('button[type="submit"]');
if (button) button.click();
});
await new Promise(resolve => setTimeout(resolve, 5000));
}Ưu điểm:
- Tự động điền OTP, tăng hiệu quả tự động hóa
- Đảm bảo trang vẫn đang ở trạng thái 2FA, tránh lỗi chuyển trang sớm
- Mô phỏng thao tác nhấn nút thực tế, giảm rủi ro bị kiểm tra bảo mật
Bước 6: Kiểm tra kết quả đăng nhập cuối cùng và gửi Signal
const finalUrl = page.url();
const isLoggedIn =
!finalUrl.includes('/sessions/verified-device') &&
!finalUrl.includes('/login') &&
(finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
await client.send('Signal.send', {
event: 'github_login_result',
data: JSON.stringify({
success: isLoggedIn,
username: githubCredentials.username,
url: finalUrl,
twoFactorCode: githubCredentials.twoFactorCode,
timestamp: new Date().toISOString()
})
});
if (isLoggedIn) await page.goto('https://github.com/', { waitUntil: 'networkidle2' });Ưu điểm:
- Tránh nhầm lẫn đánh giá, đảm bảo trang đã đăng nhập thành công
- Tự động điều hướng về trang chủ GitHub để xác nhận, tăng độ tin cậy
- Kết quả có thể báo cáo real-time đến CI/CD hoặc hệ thống bot
Bước 7: Duy trì phiên làm việc và đóng trình duyệt
await new Promise(resolve => setTimeout(resolve, 5000));
await browser.close();Ưu điểm:
- Đảm bảo mọi sự kiện Signal đã được gửi đi
- Giữ phiên hoạt động đủ lâu cho các thao tác kế tiếp
- Đóng trình duyệt để tránh rò rỉ tài nguyên
Mã hoàn chỉnh
- Đầu tiên, bạn sử dụng script này để thực hiện điền username, password trang đăng nhập GitHub và đợi nhập OTP trên cửa sổ xác minh Email.
Bạn sẽ nhận được taskId khi tạo tác vụ phiên trình duyệt, hãy nhớ taskId này cho bước tiếp theo.
import puppeteer from 'puppeteer-core';
const token = "api-key";
const query = new URLSearchParams({
token,
proxyCountry: "ANY",
sessionRecording: true,
sessionTTL: 900,
sessionName: "Data Scraping",
});
const createBrowserSessionURL = `https://browser.scrapeless.com/api/v2/browser?${query.toString()}`;
// Lấy taskId session qua HTTP API
const sessionResponse = await fetch(createBrowserSessionURL);
const {taskId} = await sessionResponse.json();
console.log('Phiên đã tạo với task ID:', taskId);
const browserWSEndpoint = `wss://api.scrapeless.com/browser/${taskId}?x-api-key=${token}`;
async function githubLoginWith2FA() {
const browser = await puppeteer.connect({browserWSEndpoint});
let page;
try {
console.log("🚀 Bắt đầu quá trình đăng nhập GitHub...");
const githubCredentials = {
username: "****@gmail.com",
password: "******",
twoFactorCode: null
};
const pages = await browser.pages();
page = pages.length > 0 ? pages[0] : await browser.newPage();
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
console.log('📱 Đang điều hướng đến trang đăng nhập GitHub...');
await page.goto('https://github.com/login', {waitUntil: 'networkidle2'});
// Chờ form đăng nhập tải xong
await page.waitForSelector('#login_field', {timeout: 10000});
console.log('🔑 Đang nhập tên đăng nhập và mật khẩu...');
await page.type('#login_field', githubCredentials.username);
await page.type('#password', githubCredentials.password);
// Nhấn nút đăng nhập
console.log('🖱️ Nhấn nút đăng nhập...');
await page.click('input[type="submit"][value="Sign in"]');
// dùng setTimeout thay vì waitForTimeout
console.log('⏳ Đang chờ phản hồi trang...');
await new Promise(resolve => setTimeout(resolve, 3000));
// Kiểm tra xem có yêu cầu xác thực email (2FA) hay không
const currentUrl = page.url();
console.log(`🔍 URL hiện tại: ${currentUrl}`);
if (currentUrl.includes('/sessions/verified-device')) {
console.log('🔐 Phát hiện yêu cầu xác thực email, đợi mã xác thực...');
const client = await page.target().createCDPSession();
// gửi tín hiệu thông báo cần mã xác thực email
await client.send('Signal.send', {
event: 'github_2fa_required',
data: JSON.stringify({
status: '2fa_required',
timestamp: new Date().toISOString()
})
});
// Chờ nhận mã xác thực email
console.log('⏳ Đang chờ mã xác thực email...');
const twoFactorResult = await client.send('Signal.wait', {
event: 'github_2fa_code',
timeout: 120000
});
if (twoFactorResult.status === 200 && twoFactorResult.data) {
const twoFactorData = JSON.parse(twoFactorResult.data);
githubCredentials.twoFactorCode = twoFactorData.code;
console.log(`✅ Đã nhận mã xác thực email: ${githubCredentials.twoFactorCode}, đang nhập mã...`);
// Đảm bảo vẫn ở trang xác thực
if (!page.url().includes('/sessions/verified-device')) {
console.log('⚠️ Trang đã chuyển hướng, có thể không còn cần xác thực');
return;
}
// Nhập mã xác thực
console.log('⌨️ Đang nhập mã xác thực...');
await page.$eval('#otp', (input, code) => {
input.value = '';
}, githubCredentials.twoFactorCode);
await page.type('#otp', githubCredentials.twoFactorCode);
console.log(`✅ Mã xác thực ${githubCredentials.twoFactorCode} đã được nhập`);
// Nhấn nút xác thực
console.log('🖱️ Nhấn nút xác thực...');
try {
await page.evaluate(() => {
const button = document.querySelector('button[type="submit"]');
if (button) button.click();
});
console.log('✅ Đã nhấn nút xác thực, đang chờ phản hồi trang...');
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (clickError) {
console.log('⚠️ Lỗi khi nhấn nút:', clickError.message);
}
// Kiểm tra kết quả đăng nhập
await new Promise(resolve => setTimeout(resolve, 3000));
const finalUrl = page.url();
console.log(`🔍 URL cuối cùng: ${finalUrl}`);
const isLoggedIn = !finalUrl.includes('/sessions/verified-device') &&
!finalUrl.includes('/login') &&
(finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
// gửi tín hiệu kết quả đăng nhập
if (client) {
await client.send('Signal.send', {
event: 'github_login_result',
data: JSON.stringify({
success: isLoggedIn,
username: githubCredentials.username,
url: finalUrl,
twoFactorCode: githubCredentials.twoFactorCode,
timestamp: new Date().toISOString()
})
});
}
if (isLoggedIn) {
console.log('🎉 Đăng nhập GitHub thành công!');
try {
await page.goto('https://github.com/', {
waitUntil: 'networkidle2',
timeout: 10000
});
console.log('✅ Truy cập trang chủ GitHub thành công');
} catch (profileError) {
console.log('⚠️ Lỗi khi truy cập trang chủ:', profileError.message);
}
} else {
console.log('❌ Xác thực email thất bại, đăng nhập không thành công');
console.log('🔍 Tiêu đề trang hiện tại:', await page.title());
}
} else {
console.log('❌ Hết thời gian chờ nhận mã xác thực email');
}
} else if (currentUrl.includes('github.com') && !currentUrl.includes('/login')) {
// Không cần xác thực email
console.log('✅ Đăng nhập thành công (không cần xác thực email)');
const client = await page.target().createCDPSession();
await client.send('Signal.send', {
event: 'github_login_result',
data: JSON.stringify({
success: true,
username: githubCredentials.username,
url: currentUrl,
timestamp: new Date().toISOString()
})
});
} else {
console.log('❌ Đăng nhập thất bại, vẫn ở trang đăng nhập');
console.log('🔍 Tiêu đề trang hiện tại:', await page.title());
}
// Giữ phiên làm việc trong thời gian ngắn
console.log('⏳ Giữ kết nối trong 5 giây...');
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (error) {
console.error('❌ Quá trình đăng nhập GitHub thất bại:', error);
try {
const pages = await browser.pages();
const currentPage = pages.length > 0 ? pages[0] : page;
if (currentPage) {
const errorClient = await currentPage.target().createCDPSession();
await errorClient.send('Signal.send', {
event: 'github_login_error',
data: JSON.stringify({
error: error.message,
timestamp: new Date().toISOString()
})
});
}
} catch (signalError) {
console.error('❌ Gửi tín hiệu lỗi cũng thất bại:', signalError);
}
} finally {
if (browser) await browser.close();
console.log('🔚 Script đăng nhập GitHub kết thúc');
}
}
// Chạy script
githubLoginWith2FA().catch(console.error);
- Khi script ở trên đến trang chờ mã xác thực, chạy ngay script lắng nghe email này, nó sẽ gửi mã mới nhất về script chính để hoàn thành xác thực.
import Imap from 'imap';
import {simpleParser} from 'mailparser';
const CONFIG = {
imap: {
user: "****@gmail.com",
password: "****",
host: "mail.privateemail.com",
port: 993,
tls: true,
tlsOptions: {rejectUnauthorized: false}
},
signal: {
baseUrl: "https://browser.scrapeless.com",
apiKey: "api-key"
},
checkInterval: 5000,
maxWaitTime: 120000
};
class EmailListener {
constructor() {
this.imap = null;
this.isListening = false;
this.sessionId = null;
}
async start(sessionId) {
console.log('Bắt đầu lắng nghe email...');
this.sessionId = sessionId;
try {
await this.connectIMAP();
const code = await this.listenForCode();
if (code) {
console.log(`Tìm thấy mã: ${code}`);
await this.sendSignal('github_2fa_code', {code});
console.log('Đã gửi mã đến trình duyệt');
} else {
console.log('Không tìm thấy mã (hết thời gian chờ)');
await this.sendSignal('email_listener_timeout', {status: 'timeout'});
}
} catch (error) {
console.error('Lỗi listener:', error.message || error);
} finally {
await this.cleanup();
}
}
connectIMAP() {
return new Promise((resolve, reject) => {
this.imap = new Imap(CONFIG.imap);
this.imap.once('ready', () => {
console.log('Kết nối IMAP thành công');
resolve();
});
this.imap.once('error', reject);
this.imap.connect();
});
}
async listenForCode() {
console.log('Đang lắng nghe mã xác thực GitHub...');
this.isListening = true;
const startTime = Date.now();
while (this.isListening && (Date.now() - startTime) < CONFIG.maxWaitTime) {
try {
const code = await this.checkEmails();
if (code) return code;
await new Promise(resolve => setTimeout(resolve, CONFIG.checkInterval));
} catch (error) {
console.error('checkEmails thất bại:', error.message || error);
await new Promise(resolve => setTimeout(resolve, 10000));
}
}
return null;
}
checkEmails() {
return new Promise((resolve, reject) => {
this.imap.openBox('INBOX', false, (err, box) => {
if (err) return reject(err);
const criteria = ['UNSEEN', ['FROM', '****@github.com']];
this.imap.search(criteria, (err, results) => {
if (err) return reject(err);
if (!results || results.length === 0) return resolve(null);
this.processEmails(results, resolve, reject);
});
});
});
}
processEmails(results, resolve, reject) {
const fetcher = this.imap.fetch(results, {
bodies: ['TEXT'],
markSeen: true
});
let processed = 0;
let foundCode = null;
fetcher.on('message', (msg) => {
let buffer = '';
msg.on('body', (stream) => {
stream.on('data', (chunk) => buffer += chunk.toString('utf8'));
});
msg.once('end', async () => {
try {
const mail = await simpleParser(buffer);
const code = this.extractCode(mail.text || '');
if (code) foundCode = code;
} catch (error) {
console.error('Phân tích mail thất bại:', error);
}
processed++;
if (processed === results.length) resolve(foundCode);
});
});
fetcher.once('error', reject);
}
extractCode(text) {
const patterns = [
/verification code:?\s*(\d{6})/i,
/verification code:?\s*(\d{6})/i,
/code:?\s*(\d{6})/i,
/GitHub verification code:?\s*(\d{6})/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) return match[1];
}
const digitMatch = text.match(/\b\d{6}\b/);
return digitMatch ? digitMatch[0] : null;
}
// Gửi tín hiệu qua HTTP, sessionId làm tham số
async sendSignal(event, data, sessionId = this.sessionId) {
if (!sessionId) throw new Error('Không có Session ID');
try {
const url = `${CONFIG.signal.baseUrl}/browser/${sessionId}/signal/send`;
const response = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'token': CONFIG.signal.apiKey
},
body: JSON.stringify({event, data})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
console.log('Gửi tín hiệu thành công:', result);
return result;
} catch (err) {
console.error('Gửi tín hiệu HTTP thất bại:', err);
throw err;
}
}
async cleanup() {
this.isListening = false;
if (this.imap) {
try {
this.imap.end();
console.log('Đóng kết nối IMAP');
} catch (e) {
console.error('Lỗi khi đóng IMAP:', e);
}
}
this.sessionId = null;
}
}
const listener = new EmailListener();
listener.start({taskId}).then(); // Thay {taskId} bằng taskId bước đầu tiên
Qua hai ví dụ đăng nhập GitHub trên, chúng tôi minh họa cách đạt được đăng nhập tự động hiệu quả và ổn định trong môi trường doanh nghiệp, bao gồm hai chế độ 2FA phổ biến: TOTP và mã xác thực Email. Sử dụng Scrapeless Browser + Signal CDP, bạn có thể thực hiện các thao tác trình duyệt thực, mô phỏng chính xác hành vi người dùng, và tương tác thời gian thực với hệ thống MFA và listener email để tự động lấy và nhập mã xác thực. Cho dù là phát triển workflow đăng nhập tự động, tích hợp với hệ thống CI/CD, hay quản lý tài khoản nội bộ doanh nghiệp, giải pháp này giúp tăng tỷ lệ thành công đăng nhập, giảm can thiệp thủ công và cung cấp đầy đủ audit cũng như giám sát vận hành.