2026年5月28日

2026年5月28日

WordPressに二要素認証(2FA)を実装する方法

はじめに

「WordPressの管理者アカウントがパスワードだけで保護されていて不安」「不正ログイン試行が多くセキュリティを強化したい」「Google AuthenticatorやAuthyなどのアプリでWordPressにログインしたい」——二要素認証(2FA)でパスワード漏洩後の不正ログインを防げます。

症状・原因

パスワードだけの認証は辞書攻撃・ブルートフォース攻撃・フィッシングで突破されるリスクがあります。TOTPベースの2FAはパスワードに加えて時刻同期の6桁コードを要求するため、パスワードが漏洩しても不正ログインを防止できます。

解決手順

ステップ1:TOTPシークレットキーを生成してユーザーに紐付ける

// composer.json で TOTP ライブラリを追加
// composer require sonata-project/google-authenticator
// または OTPHP: composer require spomky-labs/otphp

// functions.php: シークレットキーの生成と保存
function generate_totp_secret(): string {
    $chars  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32文字
    $secret = '';
    for ( $i = 0; $i < 16; $i++ ) {
        $secret .= $chars[ random_int( 0, 31 ) ];
    }
    return $secret;
}

// ユーザーの2FA設定を保存
function save_2fa_secret( int $user_id, string $secret ): void {
    update_user_meta( $user_id, '_2fa_secret', $secret );
    update_user_meta( $user_id, '_2fa_enabled', false ); // 確認後にtrueに
}

// ユーザープロファイルページに2FA設定フォームを追加
add_action( 'show_user_profile', 'render_2fa_settings' );
add_action( 'edit_user_profile', 'render_2fa_settings' );

function render_2fa_settings( WP_User $user ): void {
    $secret  = get_user_meta( $user->ID, '_2fa_secret', true );
    $enabled = (bool) get_user_meta( $user->ID, '_2fa_enabled', true );

    if ( ! $secret ) {
        $secret = generate_totp_secret();
        save_2fa_secret( $user->ID, $secret );
    }

    $site_name = rawurlencode( get_bloginfo( 'name' ) );
    $email     = rawurlencode( $user->user_email );
    $otpauth   = "otpauth://totp/{$site_name}:{$email}?secret={$secret}&issuer={$site_name}";
    ?>
    <h2>二要素認証(2FA)</h2>
    <table class="form-table">
        <tr>
            <th>状態</th>
            <td><?php echo $enabled ? '<strong style="color:green">有効</strong>' : '<strong style="color:red">無効</strong>'; ?></td>
        </tr>
        <tr>
            <th>QRコード</th>
            <td>
                <div id="qr-code-2fa"></div>
                <p class="description">Google Authenticator / Authy でスキャン</p>
                <script>
                    // QRコード表示(qrcode.js を使用)
                    document.addEventListener('DOMContentLoaded', function() {
                        if (typeof QRCode !== 'undefined') {
                            new QRCode(document.getElementById('qr-code-2fa'), {
                                text: '<?php echo esc_js( $otpauth ); ?>',
                                width: 200, height: 200
                            });
                        }
                    });
                </script>
            </td>
        </tr>
        <tr>
            <th>確認コード</th>
            <td>
                <input type="text" name="2fa_verify_code" maxlength="6" placeholder="6桁のコード">
                <?php wp_nonce_field( '2fa_setup_' . $user->ID, '2fa_nonce' ); ?>
            </td>
        </tr>
    </table>
    <?php
}

ステップ2:TOTPコードを検証する

// TOTP検証関数(外部ライブラリなしの実装)
function verify_totp( string $secret, string $code, int $discrepancy = 1 ): bool {
    $code = preg_replace( '/\s/', '', $code );
    if ( strlen( $code ) !== 6 || ! ctype_digit( $code ) ) {
        return false;
    }

    $timestamp = (int) floor( time() / 30 );

    for ( $i = -$discrepancy; $i <= $discrepancy; $i++ ) {
        if ( hash_equals( (string) generate_totp_code( $secret, $timestamp + $i ), $code ) ) {
            return true;
        }
    }
    return false;
}

function generate_totp_code( string $secret, int $counter ): string {
    // Base32デコード
    $base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    $secret      = strtoupper( $secret );
    $binary      = '';
    $buffer      = 0;
    $bits_left   = 0;

    for ( $i = 0; $i < strlen( $secret ); $i++ ) {
        $pos      = strpos( $base32chars, $secret[ $i ] );
        if ( false === $pos ) continue;
        $buffer    = ( $buffer << 5 ) | $pos;
        $bits_left += 5;
        if ( $bits_left >= 8 ) {
            $binary    .= chr( ( $buffer >> ( $bits_left - 8 ) ) & 0xFF );
            $bits_left -= 8;
        }
    }

    // HMAC-SHA1
    $time   = pack( 'N*', 0 ) . pack( 'N*', $counter );
    $hash   = hash_hmac( 'sha1', $time, $binary, true );
    $offset = ord( $hash[19] ) & 0xF;
    $code   = (
        ( ord( $hash[ $offset     ] ) & 0x7F ) << 24 |
        ( ord( $hash[ $offset + 1 ] ) & 0xFF ) << 16 |
        ( ord( $hash[ $offset + 2 ] ) & 0xFF ) <<  8 |
        ( ord( $hash[ $offset + 3 ] ) & 0xFF )
    ) % 1000000;

    return str_pad( (string) $code, 6, '0', STR_PAD_LEFT );
}

// プロファイル保存時に2FA有効化
add_action( 'personal_options_update',  'save_2fa_profile' );
add_action( 'edit_user_profile_update', 'save_2fa_profile' );

function save_2fa_profile( int $user_id ): void {
    if ( ! isset( $_POST['2fa_nonce'] ) ) return;
    if ( ! wp_verify_nonce( $_POST['2fa_nonce'], '2fa_setup_' . $user_id ) ) return;
    if ( ! current_user_can( 'edit_user', $user_id ) ) return;

    $code   = sanitize_text_field( $_POST['2fa_verify_code'] ?? '' );
    $secret = get_user_meta( $user_id, '_2fa_secret', true );

    if ( $code && $secret && verify_totp( $secret, $code ) ) {
        update_user_meta( $user_id, '_2fa_enabled', true );
        // バックアップコードを生成
        $backup_codes = generate_backup_codes( $user_id );
        add_settings_error( '2fa', '2fa_enabled', '2FAを有効化しました。バックアップコード: ' . implode( ', ', $backup_codes ), 'success' );
    }
}

ステップ3:ログインフローに2FA検証を追加する

// ログイン成功後に2FAコード入力を要求
add_filter( 'authenticate', function( $user, string $username, string $password ) {
    if ( is_wp_error( $user ) || ! ( $user instanceof WP_User ) ) {
        return $user;
    }

    $enabled = (bool) get_user_meta( $user->ID, '_2fa_enabled', true );
    if ( ! $enabled ) {
        return $user; // 2FA無効ならそのままログイン
    }

    // セッションにユーザーIDを一時保存して2FAページにリダイレクト
    if ( ! session_id() ) session_start();
    $_SESSION['2fa_pending_user'] = $user->ID;
    $_SESSION['2fa_timestamp']    = time();

    // 2FAコード入力ページにリダイレクト(後でwp_loginフックで処理)
    wp_redirect( wp_login_url() . '?action=2fa' );
    exit;
}, 30, 3 );

// 2FAコード入力フォームの処理
add_action( 'login_form_2fa', function(): void {
    if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
        if ( ! session_id() ) session_start();
        $pending_id = $_SESSION['2fa_pending_user'] ?? 0;
        $timestamp  = $_SESSION['2fa_timestamp']    ?? 0;

        // セッション有効期限(5分)
        if ( ! $pending_id || time() - $timestamp > 300 ) {
            wp_redirect( wp_login_url() );
            exit;
        }

        $code   = sanitize_text_field( $_POST['2fa_code'] ?? '' );
        $secret = get_user_meta( $pending_id, '_2fa_secret', true );

        if ( verify_totp( $secret, $code ) || verify_backup_code( $pending_id, $code ) ) {
            unset( $_SESSION['2fa_pending_user'], $_SESSION['2fa_timestamp'] );
            $user = get_user_by( 'id', $pending_id );
            wp_set_auth_cookie( $user->ID, false );
            wp_redirect( admin_url() );
            exit;
        }

        // 失敗
        wp_redirect( wp_login_url() . '?action=2fa&error=1' );
        exit;
    }
} );

ステップ4:バックアップコードを生成・検証する

function generate_backup_codes( int $user_id, int $count = 8 ): array {
    $codes        = [];
    $hashed_codes = [];

    for ( $i = 0; $i < $count; $i++ ) {
        $code           = strtoupper( bin2hex( random_bytes( 4 ) ) ); // 8文字
        $codes[]        = $code;
        $hashed_codes[] = wp_hash_password( $code );
    }

    update_user_meta( $user_id, '_2fa_backup_codes', $hashed_codes );
    return $codes; // 平文は一度だけ表示
}

function verify_backup_code( int $user_id, string $code ): bool {
    $code         = strtoupper( preg_replace( '/[^A-Z0-9]/', '', $code ) );
    $stored_codes = get_user_meta( $user_id, '_2fa_backup_codes', true );

    if ( ! is_array( $stored_codes ) ) return false;

    foreach ( $stored_codes as $index => $hashed ) {
        if ( wp_check_password( $code, $hashed ) ) {
            // 使用済みコードを削除
            unset( $stored_codes[ $index ] );
            update_user_meta( $user_id, '_2fa_backup_codes', array_values( $stored_codes ) );
            return true;
        }
    }
    return false;
}

ステップ5:Two Factor プラグインを使う場合

# 公式の Two Factor プラグインを使う(推奨)
wp plugin install two-factor --activate

# 管理者ユーザーに2FAを強制
wp user meta update 1 _two_factor_provider "Two_Factor_Totp"

# 2FAを無効化(ロックアウト時の緊急対応)
wp user meta delete 1 _two_factor_enabled_providers
wp user meta delete 1 _two_factor_provider
// Two Factor プラグイン使用時: 管理者ロールに2FAを強制
add_filter( 'two_factor_enabled_providers_for_user', function(
    array   $providers,
    WP_User $user
): array {
    // 管理者には2FAを必須化
    if ( in_array( 'administrator', $user->roles, true ) && empty( $providers ) ) {
        // プロバイダーが設定されていない場合はメール2FAにフォールバック
        return [ 'Two_Factor_Email' => 'Two_Factor_Email' ];
    }
    return $providers;
}, 10, 2 );

注意事項

  • 2FAを設定した後、自分自身がロックアウトされないようにバックアップコードを必ず安全な場所に保存してください。ロックアウト時はWP-CLIまたはデータベース直接操作でリセットできます。
  • TOTPは時刻同期に依存します。サーバーの時刻がずれると認証が失敗します。ntpdまたはchronyでサーバー時刻を正確に保ってください。
  • カスタム実装よりTwo Factor公式プラグインの使用を推奨します。TOTP・メール・WebAuthn(パスキー)などに対応しています。

まとめ

2FA実装は「TOTPシークレット生成→ユーザーメタに保存→QRコードをプロファイルページに表示→HMAC-SHA1でTOTPコードを検証→authenticateフィルターでログインフローに挿入→バックアップコードをwp_hash_passwordでハッシュ保存→緊急時はWP-CLIでリセット」の流れで整備します。関連記事:WordPressにセキュリティヘッダーを設定する方法WordPressのXML-RPC攻撃対策をする方法

お気軽にご相談ください

お見積りへ お問い合わせへ