BrowserガイドGitHub MFA 自動化

GitHub MFA自動化

はじめに

GitHubログイン自動化で最大の課題は**二要素認証(2FA)**です。Authenticatorアプリ(TOTP)やEmail OTPであっても、従来の自動化フローは通常以下の理由でここで詰まります:

  • 検証コードの自動取得ができない
  • コードのリアルタイム同期ができない
  • 自動化によるコード入力ができない
  • ブラウザ環境の非現実性によりGitHubのセキュリティチェックを誘発してしまう

本記事ではScrapeless Browser + Signal CDPを使ったGitHub 2FAの完全自動化ワークフローを示します。対象は:

  • ケース1: GitHub 2FA(Authenticator / TOTP自動生成)
  • ケース2: GitHub 2FA(Email OTP自動受信)

それぞれのフローを解説し、ログインスクリプトと認証コードリスナーを統合した自動化システムの構築方法を紹介します。


ケース1: GitHub 2FA(Authenticator OTPモード)

GitHubの**TOTP(Time-based One-Time Password)**は自動化に理想的な方式です。Scrapeless Browser + Signal CDPを使うとブラウザは自動的に:

  • 2FAページ到達を検知
  • OTPコードを生成
  • コードを自動入力
  • ログイン完了

従来のEmail/SMS OTPと比べて:

  • 外部依存なしでローカル生成可能
  • コード生成も高速かつ安定
  • 追加API不要
  • 人手介入なしで完全自動化可能

適用例:

  • Google Authenticator / Authy / 1Passwordを使ったGitHubアカウント
  • 2FA画面パス:/sessions/two-factor/app または /sessions/two-factor/webauthn

動画

Case 1: GitHub 2FA (Authenticator OTP Mode)

ステップ1: Scrapeless Browserへ接続

まずScrapeless Browserへ全二重WebSocket接続を確立します:

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("✅ Scrapeless Browserに接続完了");

利点:

  • 強力なアンチ検知機能を持つクラウドのリアルChrome
  • ローカルリソースを消費しない
  • 自動プロキシ・永続化・セッション録画対応
  • 大規模自動化ログインの信頼性が高い

ステップ2: MFAマネージャとTOTPプロバイダ初期化

MFAはSignal CDPによる双方向通信で処理します。これにより:

  • 2FAページ検出時に自動でmfa_code_request送信
  • TOTPコードの生成
  • mfa_code_responseでコード返送
  • ログイン結果イベント送信
import { authenticator } from 'otplib';
 
const TOTP_SECRETS = {
    'github': 'secret-code',
    'default': 'secret-code'
};
 
authenticator.options = {
    digits: 6,
    step: 30,
    window: 1
};

ステップ3: MFAマネージャ作成

Signal CDPとの通信を一元管理するMFAマネージャを作成します:

  • mfa_code_requestをトリガー
  • 検証コード受信待ち・取得(mfa_code_response
  • GitHubログイン処理へ返却
  • 最終ログイン結果イベント送信
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('MFAコード待機タイムアウト');
        } else {
            throw new Error(`Signal.wait失敗、ステータス: ${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)
        });
    }
}

利点:

  • 完全自動の2FAワークフロー(リクエスト→待機→取得→入力)が実現
  • 複数認証方式(TOTP/Email/Push)も一元管理
  • スクリプト内の2FA重複コードを排除
console.log("🚀 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('📱 GitHubログインページに移動しています...');
        await page.goto('https://github.com/login', {waitUntil: 'networkidle2'});
        
        // ログインフォームが読み込まれるのを待つ
        await page.waitForSelector('#login_field', {timeout: 10000});
        
        console.log('🔑 ユーザー名とパスワードを入力しています...');
        await page.type('#login_field', githubCredentials.username);
        await page.type('#password', githubCredentials.password);
        
        // サインインボタンをクリック
        console.log('🖱️ サインインボタンをクリックしています...');
        await page.click('input[type="submit"][value="Sign in"]');
        
        // waitForTimeoutの代わりにsetTimeoutを使用
        console.log('⏳ ページの応答を待っています...');
        await new Promise(resolve => setTimeout(resolve, 3000));
        
        // メール認証(2FA)が必要か確認
        const currentUrl = page.url();
        console.log(`🔍 現在のURL: ${currentUrl}`);
        
        if (currentUrl.includes('/sessions/verified-device')) {
            console.log('🔐 メール認証が必要であることを検出しました。認証コードを待っています...');
            
            const client = await page.target().createCDPSession();
            
            // メール認証コードの要求を通知するシグナルを送信
            await client.send('Signal.send', {
                event: 'github_2fa_required',
                data: JSON.stringify({
                    status: '2fa_required',
                    timestamp: new Date().toISOString()
                })
            });
            
            // メール認証コードを受け取るのを待つ
            console.log('⏳ メール認証コードを待っています...');
            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(`✅ メール認証コードを受信しました: ${githubCredentials.twoFactorCode}, コードを入力しています...`);
                
                // まだ認証ページにいることを確認
                if (!page.url().includes('/sessions/verified-device')) {
                    console.log('⚠️ ページが遷移しました。認証が不要になった可能性があります');
                    return;
                }
                
                // 認証コードを入力
                console.log('⌨️ 認証コードを入力中...');
                await page.$eval('#otp', (input, code) => {
                    input.value = '';
                }, githubCredentials.twoFactorCode);
                
                await page.type('#otp', githubCredentials.twoFactorCode);
                console.log(`✅ 認証コード ${githubCredentials.twoFactorCode} を入力しました`);
                
                // 確認ボタンをクリック
                console.log('🖱️ 確認ボタンをクリックしています...');
                try {
                    await page.evaluate(() => {
                        const button = document.querySelector('button[type="submit"]');
                        if (button) button.click();
                    });
                    
                    console.log('✅ 確認ボタンをクリックしました。ページの応答を待っています...');
                    await new Promise(resolve => setTimeout(resolve, 5000));
                    
                } catch (clickError) {
                    console.log('⚠️ ボタンのクリックに問題があります:', clickError.message);
                }
                
                // ログイン結果をチェック
                await new Promise(resolve => setTimeout(resolve, 3000));
                const finalUrl = page.url();
                console.log(`🔍 最終URL: ${finalUrl}`);
                
                const isLoggedIn = !finalUrl.includes('/sessions/verified-device') &&
                    !finalUrl.includes('/login') &&
                    (finalUrl.includes('github.com') || finalUrl === 'https://github.com/');
                
                // ログイン結果のシグナルを送信
                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('🎉 GitHubログインに成功しました!');
                    try {
                        await page.goto('https://github.com/', {
                            waitUntil: 'networkidle2',
                            timeout: 10000
                        });
                        console.log('✅ GitHubホームページへ正常にアクセスしました');
                    } catch (profileError) {
                        console.log('⚠️ ホームページアクセスに問題があります:', profileError.message);
                    }
                } else {
                    console.log('❌ メール認証に失敗しました。ログインできませんでした');
                    console.log('🔍 現在のページタイトル:', await page.title());
                }
                
            } else {
                console.log('❌ メール認証コードの受信がタイムアウトしました');
            }
            
        } else if (currentUrl.includes('github.com') && !currentUrl.includes('/login')) {
            // メール認証不要
            console.log('✅ ログイン成功(メール認証不要)');
            
            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('❌ ログイン失敗。まだログインページにいます');
            console.log('🔍 現在のページタイトル:', await page.title());
        }
        
        // セッションを短時間保持
        console.log('⏳ 接続を5秒間維持しています...');
        await new Promise(resolve => setTimeout(resolve, 5000));
        
    } catch (error) {
        console.error('❌ GitHubログインプロセスに失敗しました:', 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('❌ エラーシグナルの送信にも失敗しました:', signalError);
        }
        
    } finally {
        if (browser) await browser.close();
        console.log('🔚 GitHubログインスクリプトが終了しました');
    }
}
 
// スクリプトを実行
githubLoginWith2FA().catch(console.error);
 
  1. 上記のスクリプトが認証コード待ちのページに達したら、即座に以下のメールリスナースクリプトを実行してください。これにより最新のコードがメインスクリプトに送信され、認証が完了します。
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('メールリスナーを開始しています...');
        this.sessionId = sessionId;
 
        try {
            await this.connectIMAP();
            const code = await this.listenForCode();
 
            if (code) {
                console.log(`コードを発見しました: ${code}`);
                await this.sendSignal('github_2fa_code', {code});
                console.log('コードをブラウザに送信しました');
            } else {
                console.log('コードが見つかりませんでした(タイムアウト)');
                await this.sendSignal('email_listener_timeout', {status: 'timeout'});
            }
        } catch (error) {
            console.error('リスナーエラー:', 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('IMAPに接続しました');
                resolve();
            });
 
            this.imap.once('error', reject);
            this.imap.connect();
        });
    }
 
    async listenForCode() {
        console.log('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に失敗:', 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('メールの解析に失敗しました:', 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;
    }
 
    // HTTP経由でシグナルを送信、sessionIdはパラメータ
    async sendSignal(event, data, sessionId = this.sessionId) {
        if (!sessionId) throw new Error('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('シグナル送信に成功しました:', result);
            return result;
        } catch (err) {
            console.error('シグナル送信に失敗しました:', err);
            throw err;
        }
    }
 
    async cleanup() {
        this.isListening = false;
        if (this.imap) {
            try {
                this.imap.end();
                console.log('IMAP接続を終了しました');
            } catch (e) {
                console.error('IMAP終了時のエラー:', e);
            }
        }
        this.sessionId = null;
    }
}
 
const listener = new EmailListener();
listener.start({taskId}).then(); // 最初のステップからtaskIdを置き換えてください
 

上記の2つのGitHubログイン例を通じて、エンタープライズ環境で効率的かつ安定した自動ログインを実現する方法を示しています。2つの一般的な2FA方式であるTOTPおよびメール認証コードをカバーしています。Scrapeless Browser + Signal CDPを使うことで、実際のブラウザ操作を行い、ユーザー行動を精確にシミュレートし、MFAシステムやメールリスナーとリアルタイムに連携して認証コードの自動取得・送信が可能です。自動ログインワークフローの開発やCI/CDシステムとの統合、企業内アカウント管理などにおいて、ログイン成功率の大幅向上、手動介入の削減、完全な操作監査およびモニタリングの提供が可能となります。