前章までの施策によって、フロントエンド側に出力されるコンテンツ系ページからWordPressっぽさを排除できた。だが、ログイン画面などの認証系周りは未施策なため、この章ではその辺の秘匿化を行なっていく。

 まず、現状の確認だが、今までの施策によって、WEBサイトのドキュメントルート直下にはWordPressのログイン画面であるwp-login.phpは存在していない。そのため、仮にhttps://{Site_Domain_Name}/wp-login.phpに直接アクセスされたとしても、HTTP CODEとして404 NOT FOUNDが返される。しかし、WordPressのインストールパスを含めたhttps://{Site_Domain_Name}/app/wp-login.phpにアクセスするとログイン画面が表示されてしまう。サイトの秘匿化にあたっては、すべからくwp-login.phpの存在を隠蔽してしまうのが最適解となる。

wp-login.phpの存在を消す

 このファイルがあるだけで、そのサイトがWordPress建てであることがバレてしまう。さらには、このファイル名によるログイン画面の存在は、不正アクセスやクラッキングの温床になるのでデメリットしかない。であれば、もう一律でwp-login.phpへのアクセスを禁止して、その存在を亡きものにしてしまおう。
 .htaccessを下記のように変更する。

# ---
# 7. wp-login.phpへのアクセスは、接続元に依らず一律で404レスポンスを返す
# ---
RewriteRule wp-login\.php(.*)$ - [R=404,L]

 これで、URLにwp-login.phpが含まれるアクセスはすべて404となる。
 同時に、誰もWordPressにログインできなくなる(笑)。

新たなログイン用URLを作る

 では、wp-login.phpに代る新しいログイン画面を作っていこう。この作り方には様々な方法があるが、今回はwp-login.phpをラップして新しいログイン用URLを作る方法を紹介する。
 この方法は、Login rebuilderプラグイン等で採用されているやり方で、WordPressのログイン画面URLを変更する方法としては結構メジャーなものではないだろうか。
 まず、ドキュメントルートに新しいログイン画面となるPHPファイルを作ろう。今回のチャレンジではわかりやすくentrance.phpとする。実際に運用するWebサイトでは、例えば憶えられづらいハッシュ値のような文字列のファイル名にしておくと、セキュリティ的には有用かもしれない。しかし、保守しづらくなるので一長一短ではあるが……。

$ cd {Document_Root_Path}
$ touch entrance.php

 作成したentrance.phpの中身は、下記のようにする。

<?php
$allow_ips = [
    // ログイン画面にアクセスを許可するIPアドレスを定義(未定義の場合は全ての接続元を許可する)
    // cf. '123.123.123.123', ...
];
if ( empty( $allow_ips ) || ( ! empty( $allow_ips ) && in_array( $_SERVER['REMOTE_ADDR'], $allow_ips, true ) ) ) {
    define( 'LOGIN_PAGE_FILE', basename( $_SERVER['SCRIPT_FILENAME'] ) );
    define( 'LOGIN_CREDENTIAL', hash( 'sha512', $_SERVER['HTTP_HOST'] .':'. $_SERVER['DOCUMENT_ROOT'] ) );
    require_once dirname( __FILE__ ) .'/app/wp-login.php';
} else {
    require_once dirname( __FILE__ ) . '/app/wp-load.php';
    global $wp_query;
    $wp_query->set_404();
    status_header( '404' );
    nocache_headers();
    include( get_query_template( '404' ) );
    exit;
}

 これで、https://{Site_Domain_Name}/entrance.phpが新たなログイン画面となる。しかし、このままではまだログインできないので、conceal.phpに下記のフィルターフックとアクションフックを追加する必要がある。

// WordPressインストールディレクトリの定義
$_wp_install_dir = trim( str_replace( $_SERVER['DOCUMENT_ROOT'], '', ABSPATH ), '/' );
$_wp_install_dir .= ! empty( $_wp_install_dir ) ? '/' : '';
defined( 'WP_INSTALL_DIR' ) or define( 'WP_INSTALL_DIR', $_wp_install_dir );
// 新しいログインページのファイル名を定義
defined( 'LOGIN_PAGE_FILE' ) or define( 'LOGIN_PAGE_FILE', 'entrance.php' );

add_filter( 'login_url', function( $login_url, $redirect, $force_reauth ) {
    $login_url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $login_url );
    return $login_url;
}, 10, 3 );
add_filter( 'logout_url', function( $logout_url, $redirect ) {
    $logout_url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $logout_url );
    return $logout_url;
}, 10, 2 );
add_filter( 'register_url', function( $url ) {
    $url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $url );
    return $url;
}, 10 );
add_filter( 'lostpassword_url', function( $url ) {
    $url = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $url );
    return $url;
}, 10 );
add_filter( 'site_url', function( $url, $path, $orig_scheme, $blog_id ) {
    if ( $path === 'wp-login.php' && ( is_user_logged_in() || strpos( $_SERVER['REQUEST_URI'], LOGIN_PAGE_FILE ) !== false ) ) {
        $url = str_replace( WP_INSTALL_DIR . $path, LOGIN_PAGE_FILE, $url );
    }
    return $url;
}, 10, 4 );
add_filter( 'wp_redirect', function( $location, $status ) {
    if ( strpos( $_SERVER['REQUEST_URI'], LOGIN_PAGE_FILE ) !== false ) {
        $location = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $location );
    }
    return $location;
}, 10, 2 );

add_action( 'login_init', function() {
    // 新ログインページを認証
    if ( ! defined( 'LOGIN_CREDENTIAL' ) || hash( 'sha512', $_SERVER['HTTP_HOST'] .':'. $_SERVER['DOCUMENT_ROOT'] ) !== LOGIN_CREDENTIAL ) {
        status_header( 404 );
        exit;
    }
});

 これでentrance.phpでWordPressにログイン(ログアウト、登録、パスワードリマインドも可能)ができるようになった。この方法のメリットは設定が簡単なことと、新たにログイン画面としたphpファイルが従来のwp-login.phpと同じように取り扱えるため、例えWordPressコアがアップデートされたとしてもそのまま運用していける点だ。反面、この新ログイン画面は実体はあくまでwp-login.phpなので、wp-login.phpにおける数々の制約もそのまま継承される。これがデメリットでもあり、基本的にWordPressのwp-login.phpはテーマから独立しているコアファイルの一種であるため、かなりカスタマイズがしにくい特性を持っている。PHPファイル内でページのHTML生成も行われるため、コアソースに手を加えずにログインフォーム等のUIを変更するのには限界があるのだ。
 WordPressにログインできるユーザがWeb担当者だけなどの限定的なメンバーに限られる場合など、ログイン画面自体の見た目まで隠蔽する必要がないのであれば、この方法で十分目的を達成できるだろう。しかし、不特定多数のユーザを登録・ログインさせるようなサブスクライブ型のサイトでは、この方法だけでのサイト秘匿化は完全ではないためおすすめできない。

WordPressの固定ページとして新しいログイン画面を作る

 前項で設置した新たなログイン用URLをさらにラップする形で、公開用のログイン画面を作っていこう。正確にはログイン用のフォームを備えた固定ページを作成して、そのページをログイン画面として代替利用するのだ。これによって、カスタマイズし難いwp-login.phpのような制約を受けることなくログイン画面を自由にカスタマイズできるようになる。
 一方で、ログイン画面を公開するということは、その画面に対しての第三者からの攻撃を回避できなくなることでもあるため、サイト自体のセキュリティ性は低下してしまう。この点はwp-login.phpの秘匿化効果とは別軸の問題ではあるが、そのデメリットを理解したうえでログイン画面の公開はするべきである。

 まず、WordPressにログインしたら、「固定ページ」を「新規追加」する。今回のチェレンジでは、ページのタイトルを「サインイン」に、URLスラッグをsigninとした。この固定ページを公開すると、https://{Site_Domain_Name}/signin/でこのページにアクセスできるようになる。これが新しく公開するログイン画面となるのだ。
 さて次に、この新ログイン画面の固定ページにログイン用のフォーム等を設置する。この設置方法は様々あるが、今回はブロック方式とテンプレート方式の2通りを紹介しよう。

カスタムブロックとしてログインフォームを設置する

 まず一つ目は、WordPress 5.x以降で実装された新エディタ「Gutenberg」のブロック機能を使用してログイン用の固定ページにログインフォームを設置していくやり方だ。
 自分でオリジナルのカスタムブロック定義用のJavaScriptを準備して追加しても良いが、ReactやJSX記法の知見が必要になって来るうえに結構手間なので、今回のチャレンジではプラグインを使ってGutenbergのカスタムブロックを拡張する。早速、プラグイン「Advanced Gutenberg」をインストールして、有効化しよう。
 すると、投稿編集時に追加できるブロックに「Advanced Gutenberg」カテゴリが増えるので、そこから「Login/Register Form」を選ぶ(下図参照)。

Advanced Gutenberg: blocks

 あとは、適宜入力フィールドのラベル名などを必要に応じて変更すればOkだ。実際のフロントエンド側に表示されるページを確認してみると、下図のように表示されるはずだ。

Advanced Gutenberg: Login Form

 これで、この固定ページからWordPressにログインできるようになる。ただ、ユーザ登録やログアウト、パスワードリマインダの処理は不完全なので、それらの画面用の対応が必要になる。それぞれの対応方法も紹介しておこう。

ログインおよびログアウトのリダイレクト先の調整

 新しいログイン画面でログインを行うと、ログイン完了後に管理画面にリダイレクトされない(Adminbarは表示されるので、そこから管理画面へ遷移することは可能)。また、管理画面からログアウトした時は、entrance.phpへリダイレクトされてしまい、ラップされているwp-login.phpが表示されてしまう。これらのリダイレクト先を制御するために、conceal.phpへ下記のフィルターを追加しよう。

add_filter( 'login_redirect', function( $redirect_to, $request, $user ) {
    if ( isset( $user->roles ) && is_array( $user->roles ) ) {
        if ( in_array( 'administrator', $user->roles, true ) ) {
            return home_url( WP_INSTALL_DIR .'/wp-admin' );
        } else {
            return home_url( '/' );
        }
    } else {
        return $redirect_to;
    }
}, 10, 3 );
add_filter( 'logout_redirect', function( $redirect_to, $request, $user ) {
    return home_url( '/' );
}, 10, 3 );

 これで、管理者ユーザがログインした時は管理画面へ、それ以外のユーザがログインした時はWebサイトのホームURLへそれぞれリダイレクトされるようになる。また、ログアウト時はすべてのユーザがホームURLへリダイレクトされる。
 このリダイレクト先をカスタマイズすれば、特定ユーザのみログイン後はプロフィール画面へ飛ばすということ等も可能である。

ユーザ登録時のリダイレクト処理の調整

 wp-login.phpでのユーザ登録におけるリダイレクトは、アカウント存在チェックやユーザ作成の処理にてエラーになった際に画面出力を伴う仕様になっているため、リダイレクトだけを分離することができない。そこで、ユーザ登録のコア処理が開始する前にアクションフックで独自のルーティングを挿入する必要がある(ここら辺が、wp-login.phpのカスタマイズが難しい所以だ)。下記のアクションフックをconceal.phpに追加しよう。

add_action( 'login_form_register', function() {
    $http_post = ( 'POST' === $_SERVER['REQUEST_METHOD'] );
    if ( $http_post ) {
        if ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) {
            $user_login = $_POST['user_login'];
        }
        if ( isset( $_POST['user_email'] ) && is_string( $_POST['user_email'] ) ) {
            $user_email = wp_unslash( $_POST['user_email'] );
        }
        $errors = register_new_user( $user_login, $user_email );
        if ( ! is_wp_error( $errors ) ) {
            // Redirection after completing to register user
            $redirect_to = ! empty( $_POST['redirect_to'] ) ? $_POST['redirect_to'] : home_url( 'register-complete' );
            wp_safe_redirect( $redirect_to );
            exit;
        }
    }
    wp_redirect( home_url( LOGIN_PAGE_SLUG .'/?login=failed' ) );
    exit;
});

 簡単に説明すると、wp-login.phpのユーザ登録処理を前倒ししてlogin_form_registerフックで行い、処理結果にてリダイレクトを行って、後続のコア処理をスキップさせるという建て付けだ。ただし、この対応だけでは「Advanced Gutenberg」プラグインのブロック側で、登録処理のエラーをキャッチできないので、エラー表示が正常にできないという問題は残る。これを改善するには「Advanced Gutenberg」プラグイン側の改修が必要になってくるが、それは今回のチャレンジの本筋ではないので除外する。
 さらに、WordPressのユーザ登録は初めにユーザ名とメールアドレスのみ認証し、認証完了後にログインパスワード設定用URLが記載されたメールを送信する仕様になっている。そのメールに記載されるURLもフィルタリングしておかないと、アクセスができないwp-login.phpのURLがメール通知されてしまい、誰もユーザ登録できない状況となってしまうのだ。
 まず、ユーザアカウントの認証完了画面へリダイレクトさせる必要がある(前述のソースのhome_url( 'register-complete' )の部分)ので、固定ページとして作成しよう。注意点としては、作成したページのURLスラッグをソース内に定義した値と同じくregister-completeにしておく必要があることだ。これで、ユーザ登録が成功すると下図のような画面にリダイレクトされるようになる。

Advanced Gutenberg: Register Complete

 次に、認証メールに記載されるURLをフィルタする。conceal.phpに下記を追加しよう。

add_filter( 'wp_new_user_notification_email', function( $wp_new_user_notification_email, $user, $blogname ) {
    // 登録時パスワード設定用メールの本文をフィルタ
    return str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $wp_new_user_notification_email );
}, 10, 3 );

 さらに、認証メールからアクセスされるパスワード設定画面のルーティングを行う。固定ページとして、パスワード設定用のURLスラッグがset-new-passwordのページとパスワード変更完了画面となるURLスラッグがreset-password-completeのページをそれぞれ作成する。本来、パスワード設定用のset-new-passwordのページにはパスワード入力フォームが必要なのだが、「Advanced Gutenberg」プラグインを使ってもこのフォームは作れないので、別に用意することにする。とりあえず、本文には通知メッセージと後ほどフォームを挿入するための枠だけを「カスタムHTML」ブロックで追加しておこう(下図参照)。

Gutenberg Block Editor: set-new-password page

 パスワード設定画面用のルーティングは、パスワードのバリデーション直前のアクションフックvalidate_password_resetに対して行う。

add_action( 'validate_password_reset', function( $errors, $user ) {
    list( $rp_path ) = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) );
    $rp_cookie       = 'wp-resetpass-' . COOKIEHASH;

    if ( ( ! $errors->has_errors() ) && isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) {
        reset_password( $user, $_POST['pass1'] );
        setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true );
        wp_safe_redirect( home_url( 'reset-password-complete' ) );
    } else {
        if ( isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) {
            list( $rp_login, $rp_key ) = explode( ':', wp_unslash( $_COOKIE[ $rp_cookie ] ), 2 );
        }
        wp_safe_redirect( home_url( 'set-new-password/?rp_login='. $rp_login .'&rp_key='. $rp_key ) );
    }
    exit;
}, 10, 2 );

 これで、認証メールのURLからアクセスすると固定ページのhttps://{Site_Domain_Name}/set-new-password/にリダイレクトされるようになる。仕上げとして、この固定ページに新規パスワード入力用のフォームを追加しよう。

add_filter( 'the_content', function( $content ) {
    if ( is_page( 'set-new-password' ) ) {
        $action_url  = esc_url( home_url( LOGIN_PAGE_FILE . '?action=resetpass' ) );
        $rp_login    = esc_attr( $_GET['rp_login'] );
        $rp_key      = esc_attr( $_GET['rp_key'] );
        $label_pass1 = __( 'New password' );
        $label_pass2 = __( 'Confirm new password' );
        $label_submit= __( 'Reset Password' );
        $resetpassform = <<<EOD
<form name="resetpassform" id="resetpassform" action="{$action_url}" method="post" autocomplete="off">
  <input type="hidden" id="user_login" value="{$rp_login}">
  <div>
    <label for="pass1">{$label_pass1}</label>
    <input type="password" name="pass1" id="pass1" size="24" value="" autocomplete="off">
  </div>
  <div>
    <label for="pass2">{$label_pass2}</label>
    <input type="password" name="pass2" id="pass2" size="24" value="" autocomplete="off">
  </div>
  <br>
  <input type="hidden" name="rp_key" value="{$rp_key}">
  <div>
    <button type="submit">{$label_submit}</button>
  </div>
</form>
EOD;
        $content = str_replace( '<form id="resetpassform"></form>', $resetpassform, $content );
    }
    return $content;
});

 これで、正常系のユーザ登録が可能になる。エラー時のルーティングは別途設定しないといけないが、ここまでのカスタマイズ手順とほぼ同じようなやり方で出来るはずだ。

パスワード再設定のリダイレクト処理の調整

 最後に、「パスワードをお忘れですか?」のリンクから辿るパスワード再設定のルーティングを調整していく。まず、パスワード再設定用の画面を固定ページで作成しよう。今回のチャレンジではlostpasswordというURLスラッグで作成する。このページも後ほど入力フォームを埋め込むので、「カスタムHTML」ブロックでフォーム枠だけ追加しておく(下図参照)。

Gutenberg Block Editor: lostpassword page

 そして、パスワード再設定用のルーティングを追加する。これはconceal.phplogin_form_lostpasswordlost_passwordの2つのアクションフックを利用する。

add_action( 'login_form_lostpassword', function() {
    $http_post = ( 'POST' === $_SERVER['REQUEST_METHOD'] );
    if ( $http_post ) {
        $errors = retrieve_password();
        if ( ! is_wp_error( $errors ) ) {
            // Redirection after completing to validate user
            $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : home_url( 'lostpassword-comfirm' );
            wp_safe_redirect( $redirect_to );
            exit;
        }
    } else {
        wp_safe_redirect( home_url( wp_unslash( str_replace( LOGIN_PAGE_FILE, 'lostpassword/', $_SERVER['REQUEST_URI'] ) ) ) );
        exit;
    }
});
add_action( 'lost_password', function( $errors ) {
    $lostpassword_redirect = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
    $redirect_to = apply_filters( 'lostpassword_redirect', $lostpassword_redirect );
    $get_queries = [];
    if ( ! empty( $redirect_to ) ) {
        $get_queries[] = 'redirect_to='. esc_url( $redirect_to );
    }
    if ( $errors->has_errors() ) {
        $get_queries[] = 'error='. esc_attr( $errors->get_error_code() );
    }
    wp_safe_redirect( home_url( 'lostpassword/'. ( ! empty( $get_queries ) ? '?'. implode( '&', $get_queries ) : '' ) ) );
    exit;    
}, 10 );

 次に、パスワード再設定の固定ページに入力フォームを埋め込む。前項で追加したthe_contentフィルターフックを次のように拡張しよう。

add_filter( 'the_content', function( $content ) {
    if ( is_page( 'set-new-password' ) ) {
        (...省略...)
    } else
    if ( is_page( 'lostpassword' ) ) {
        if ( isset( $_REQUEST['error'] ) && ! empty( $_REQUEST['error'] ) ) {
            switch( $_REQUEST['error'] ) {
                case 'empty_username':
                    $message = __( '<strong>Error</strong>: Enter a username or email address.' );
                    break;
                case 'invalid_email':
                    $message = __( '<strong>Error</strong>: There is no account with that username or email address.' );
                    break;
                case 'invalidkey':
                    $message = __( 'Your password reset link appears to be invalid. Please request a new link below.' );
                    break;
                case 'expiredkey':
                    $message = __( 'Your password reset link has expired. Please request a new link below.' );
                    break;
                default:
                    $message = '';
                    break;
            }
            $error = sprintf( '<div><p class="error">%s</p></div>', $message );
        }
        $action_url  = esc_url( home_url( LOGIN_PAGE_FILE . '?action=lostpassword' ) );
        $label_user_login = __( 'Username or Email Address' );
        $user_login = esc_attr( ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) ? wp_unslash( $_POST['user_login'] ) : '' );
        $lostpassword_redirect = esc_url( ( isset( $_REQUEST['redirect_to'] ) && ! empty( $_REQUEST['redirect_to'] ) ) ? $_REQUEST['redirect_to'] : '' );
        $label_submit= __( 'Get New Password' );
        $lostpasswordform = <<<EOD
{$error}
<form name="lostpasswordform" id="lostpasswordform" action="{$action_url}" method="post">
  <input type="hidden" id="user_login" value="{$rp_login}">
  <div>
    <label for="user_login">{$label_user_login}</label>
    <input type="text" name="user_login" id="user_login" value="{$user_login}" size="20" autocapitalize="off" />
  </div>
  <br>
  <input type="hidden" name="redirect_to" value="{$lostpassword_redirect}">
  <div>
    <button type="submit">{$label_submit}</button>
  </div>
</form>
EOD;
        $content = str_replace( '<form id="lostpasswordform"></form>', $lostpasswordform, $content );       
    }
    return $content;
}

 最後に、パスワード再設定でユーザ認証が成功した時に送信されるメールに記載されるパスワード再設定用のURLをフィルタリングする必要がある。これは、retrieve_password_messageのフィルターフックを追加して対応できる。

add_filter( 'retrieve_password_message', function( $message, $key, $user_login, $user_data ) {
    // パスワード変更時メールの本文をフィルタ
    $message = str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $message );
    return $message;
}, 10, 4 );

 これで完了だ。

テンプレートをインクルードしてログインフォームを設置する

 もう一つのやり方として、使用しているテーマ側にログインフォーム用のページテンプレートファイルを準備し、ログイン用の固定ページ専用のテンプレートとして設定する方法も紹介しよう。こちらは「Gutenberg」のブロックエディタに依存せずに、テーマ側でPatialテンプレートを管理できる従来のやり方に近いので、カスタマイズの柔軟性は高いかもしれない。
 まず、テーマ側にテンプレートファイルを追加する必要があるので、ログインフォーム拡張用にテンプレートを格納するディレクトリを追加しよう。今回のチャレンジはTwentyTwentyのテーマを使用しているので、そのテーマディレクトリ内に新たにpartialsのディレクトリを作ることにする。

$ cd {Document_Root_Path}
$ cd assets/themes/twentytwenty
$ mkdir -p partials

 新設したpartialsディレクトリ内にはログインフォーム拡張用のテンプレートを作成する。必要なのは、ログインフォーム、登録フォーム、パスワード設定フォーム、パスワード再設定フォーム(パスワードリマインダ)、メッセージ通知用ページの5つだ。それぞれlogin-form.phpregister-form.phprp-form.phplostpass-form.phpnotices.phpとして作成する。

$ touch partials/{{login,register,rp,lostpass}-form,notices}.php
$ ls partials/
login-form.php  lostpass-form.php  notices.php  register-form.php  rp-form.php

 これで準備完了だ。あとはそれぞれのテンプレートファイルに表示するコンテンツをコーディングしていくことになる。

ログインフォームをテンプレートから読み込む

 それでは、実際にログインフォーム用のテンプレートpartials/login-form.phpをインクルードする方式でログインフォームを実装してみよう。

<?php
/*
Template Name: Login Form
*/
get_header();
?>
<main id="site-content" role="main">
    <div class="entry-content">
        <h2 class="has-text-align-center"><?= __( 'Sign In' ) ?></h2>
        <?php get_template_part( 'partials/notices' ); ?>
        <form name="loginform" id="loginform" action="<?= esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post">
            <p>
                <label for="user_login"><?= __( 'Username or Email Address' ) ?></label>
                <input type="text" name="log" id="user_login" class="input" value="<?= isset( $_POST['log'] ) ? esc_attr( wp_unslash( $_POST['log'] ) ) : ''; ?>" size="20" autocapitalize="off">
            </p>
            <p>
                <label for="user_pass"><?= __( 'Password' ) ?></label>
                <input type="password" name="pwd" id="user_pass" class="input password-input" value="" size="20">
            </p>
            <p class="forgetmenot">
                <input name="rememberme" type="checkbox" id="rememberme" value="forever" <?php checked( ! empty( $_POST['rememberme'] ) ); ?>>
                <label for="rememberme"><?= esc_html__( 'Remember Me' ) ?></label>
            </p>
            <p class="submit">
                <input type="submit" name="login_submit" id="login_submit" class="button button-primary button-large" value="<?= esc_attr__( 'Log In' ) ?>">
                <input type="hidden" name="redirect_to" value="<?= isset( $_REQUEST['redirect_to'] ) ? esc_attr( $_REQUEST['redirect_to'] ) : ''; ?>">
                <input type="hidden" name="testcookie" value="1">
            </p>
        </form>
        <p id="nav">
<?php if ( ! isset( $_GET['checkemail'] ) || ! in_array( $_GET['checkemail'], [ 'confirm', 'newpass' ], true ) ) : ?>
<?php     if ( get_option( 'users_can_register' ) ) : ?>
            <a href="<?= esc_url( wp_registration_url() ); ?>"><?= __( 'Register' ) ?></a>
            <span>|</span>
<?php     endif; ?>
            <a href="<?= esc_url( wp_lostpassword_url() ); ?>"><?= __( 'Lost your password?' ) ?></a>
<?php endif; ?>
        </p>
    </div>
</main>
<?php
get_footer();

 次に、このテンプレートを使用した固定ページを作成する。タイトルを「サインイン」、URLスラッグをsignin、ページ属性のテンプレートを「Login Form」とする。本文など他の設定は不要だ。
 そして、conceal.phpにこのページをログインフォームとして取り扱うルーティング(ログインとログアウトそれぞれのルーティング)を設定する。

add_filter( 'login_url', function( $login_url, $redirect, $force_reauth ) {
    return home_url( 'signin/' );
}, 10, 3 );
add_filter( 'logout_url', function( $logout_url, $redirect ) {
    return str_replace( WP_INSTALL_DIR . 'wp-login.php', LOGIN_PAGE_FILE, $logout_url );
}, 10, 2 );
add_filter( 'login_redirect', function( $redirect_to, $request, $user ) {
    if ( isset( $user->roles ) && is_array( $user->roles ) ) {
        if ( in_array( 'administrator', $user->roles, true ) ) {
            $redirect_to = home_url( WP_INSTALL_DIR .'/wp-admin' );
        } else {
            $redirect_to = home_url( '/' );
        }
    }
    return $redirect_to;
}, 10, 3 );
add_filter( 'wp_login_errors', function( $errors, $redirect_to ) {
    if ( is_wp_error( $errors ) ) {
        $query_str = '';
        if ( $errors->has_errors() ) {
            $code_hash = array_map( function( $_code ){ return hash( 'crc32b', $_code ); }, $errors->get_error_codes() );
            $query_str = '?'. http_build_query( [ 'cd' => implode( ',', $code_hash ) ] );
        }
        header( 'Location: '. home_url( 'signin/?'. $query_str ), true, 307 );
        exit;
    }
    return $errors;
}, 10, 2 );
add_filter( 'logout_redirect', function( $redirect_to, $request, $user ) {
    return home_url( '/' );
}, 10, 3 );

 特殊な点としては、ログインエラー時のルーティングとして、ハッシュ化したエラーコードをGETパラメータに付与して、POST値は継承したまま307リダイレクトさせているところだ。そして、エラーコードのキャッチとエラー出力はメッセージ通知用ページとして準備したpartials/notices.phpが担うようになっている。メッセージ通知用テンプレートpartials/notices.phpは下記のように記述しよう。

<?php
/*
Template Name: Notices
*/
global $errors;
if ( ! is_wp_error( $errors ) ) {
    $errors = new WP_Error();
}
if ( function_exists( 'set_custom_errors' ) ) {
    set_custom_errors();
}
?>
        <div class="notification">
<?php if ( $errors->has_errors() ) : ?>
            <ul class="<?= empty( $errors->get_error_data() ) ? 'errors' : 'notices' ?>">
<?php   foreach ( $errors->get_error_codes() as $code ) :
          foreach ( $errors->get_error_messages( $code ) as $message ) : ?>
                <li class="message"><?= $message ?></li>
<?php     endforeach;
        endforeach; ?>
            </ul>
<?php else : ?>
            <p><?php the_content(); ?></p>
<?php endif; ?>
        </div>

 テンプレートでは、GETパラメータ等のREQUESTクエリから通知メッセージを取得して出力用にセットする関数set_custom_errors()を呼ぶようにしている。その関数はconceal.phpに定義する。

// Define notification messages related user authentication to the WP_Error
function set_custom_errors() {
    global $errors;
    // Custom notification messages
    $_messages = [
        'empty_username'          => [ __( '<strong>Error</strong>: Please enter a username.' ), '' ],
        'empty_email'             => [ __( '<strong>Error</strong>: Please type your email address.' ), '' ],
        'emptycombo'              => [ __( '<strong>Error</strong>: Please enter a username or email address.' ), '' ],
        'invalid_username'        => [ __( '<strong>Error</strong>: Sorry, that username is not allowed.' ), '' ],
        'invalid_email'           => [ __( '<strong>Error</strong>: The email address isn’t correct.' ), '' ],
        'invalidcombo'            => [ __( '<strong>Error</strong>: There is no account with that username or email address.' ), '' ],
        'username_exists'         => [ __( '<strong>Error</strong>: This username is invalid because it uses illegal characters. Please enter a valid username.' ), '' ],
        'email_exists'            => [ __( '<strong>Error</strong>: This email is already registered, please choose another one.' ), '' ],
        'invalidkey'              => [ __( 'Your password reset link appears to be invalid. Please request a new link below.' ), '' ],
        'expiredkey'              => [ __( 'Your password reset link has expired. Please request a new link below.' ), '' ],
        'password_reset_mismatch' => [ __( 'The passwords do not match.' ), '' ],
        'test_cookie'             => [ sprintf( __( '<strong>Error</strong>: Cookies are blocked or not supported by your browser. You must <a href="%s">enable cookies</a> to use WordPress.' ), '#' ), '' ],
        'expired'                 => [ __( 'Your session has expired. Please log in to continue where you left off.' ), 'message' ],
        'loggedout'               => [ __( 'You are now logged out.' ), 'message' ],
        'registerdisabled'        => [ __( 'User registration is currently not allowed.' ), '' ],
        'confirm'                 => [ __( 'Check your email for the confirmation link.' ), 'message' ],
        'newpass'                 => [ __( 'Check your email for your new password.' ), 'message' ],
        'registered'              => [ __( 'Registration complete. Please check your email.' ), 'message' ],
        'updated'                 => [ __( '<strong>You have successfully updated WordPress!</strong> Please log back in to see what’s new.' ), 'message' ],
        'enter_recovery_mode'     => [ __( 'Recovery Mode Initialized. Please log in to continue.' ), 'message' ],
        'empty_password'          => [ __( '<strong>Error</strong>: Please enter a password.' ), '' ],
        'empty_confirm_password'  => [ __( '<strong>Error</strong>: Please enter a password of confirmation.' ), '' ],
        'incorrect_password'      => [ __( '<strong>Error</strong>: Sorry, that password is not allowed.' ), '' ],
        'reseted_password'        => [ sprintf( __( 'A new password has been set. From this on, you can <a href="%s">sign in</a> using your new password.' ), wp_login_url() ), 'message' ],
        'invalid_access'          => [ __( 'The page could not be displayed due to invalid access. Please check the URL again.' ), '' ],
    ];
    if ( isset( $_REQUEST['cd'] ) && ! empty( $_REQUEST['cd'] ) ) {
        $_codes = explode( ',', trim( $_REQUEST['cd'] ) );
        foreach ( $_messages as $_code => $_data ) {
            //$_code_hash = hash( 'crc32b', $_code );
            $_code_hash = short_hash( $_code );
            if ( in_array( $_code_hash, $_codes, true ) ) {
                $errors->add( $_code, $_messages[$_code][0], $_messages[$_code][1] );
            }
        }
    } else
    if ( isset( $_REQUEST['checkemail'] ) && ! empty( $_REQUEST['checkemail'] ) ) {
        $_code = wp_unslash( trim( $_REQUEST['checkemail'] ) );
        if ( array_key_exists( $_code, $_messages ) ) {
            $errors->add( $error_code, $_messages[$_code][0], $_messages[$_code][1] );
        }
    } else
    if ( isset( $_REQUEST['password'] ) && ! empty( $_REQUEST['password'] ) ) {
        $_code = 'reseted_password';
        $errors->add( $_code, $_messages[$_code][0], $_messages[$_code][1] );
    } else
    if ( $errors->has_errors() ) {
        foreach ( $errors->get_error_codes() as $_code ) {
            $errors->add( $_code, $_messages[$_code][0], $_messages[$_code][1] );
        }
    }
    $errors = apply_filters( 'custom_notification_messages', $errors, $_REQUEST );
}

 関数内に最終的な出力用メッセージをフィルターできるcustom_notification_messagesを準備しておくと、出力メッセージ個別のカスタマイズが可能になる。これで、ログインフォームでエラーが発生した時にエラーメッセージが表示されるようになった。

登録フォームをテンプレートから読み込む

 基本的にやり方はログインフォームと同等だ。まず、登録フォーム用のテンプレートpartials/register-form.phpを下記のように更新する。

<?php
/*
Template Name: Register Form
*/
get_header();
?>
<main id="site-content" role="main">
    <div class="entry-content">
        <h2 class="has-text-align-center"><?= __( 'Sign Up' ) ?></h2>
        <?php get_template_part( 'partials/notices' ); ?>
        <form name="registerform" id="registerform" action="<?= esc_url( site_url( 'wp-login.php?action=register', 'login_post' ) ); ?>" method="post" novalidate="novalidate">
            <p>
                <label for="user_login"><?php _e( 'Username' ); ?></label>
                <input type="text" name="user_login" id="user_login" class="input" value="<?= isset( $_POST['user_login'] ) ? esc_attr( wp_unslash( $_POST['user_login'] ) ) : ''; ?>" size="20" autocapitalize="off">
            </p>
            <p>
                <label for="user_email"><?php _e( 'Email' ); ?></label>
                <input type="email" name="user_email" id="user_email" class="input" value="<?= isset( $_POST['user_email'] ) ? esc_attr( wp_unslash( $_POST['user_email'] ) ) : ''; ?>" size="25">
            </p>
            <p id="reg_passmail">
                <?= __( 'Registration confirmation will be emailed to you.' ); ?>
            </p>
            <br class="clear" />
            <input type="hidden" name="redirect_to" value="<?= isset( $_REQUEST['redirect_to'] ) ? esc_attr( $_REQUEST['redirect_to'] ) : ''; ?>">
            <p class="submit">
                <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?= esc_attr__( 'Register' ); ?>">
            </p>
        </form>
        <p id="nav">
            <a href="<?= esc_url( wp_login_url() ); ?>"><?= __( 'Log in' ); ?></a>
            <span>|</span>
            <a href="<?= esc_url( wp_lostpassword_url() ); ?>"><?= __( 'Lost your password?' ); ?></a>
        </p>
    </div>
</main>
<?php
get_footer();

 次に、このテンプレートを使用した固定ページを作成する。タイトルを「サインアップ」、URLスラッグをsignup、ページ属性のテンプレートを「Register Form」とする。本文など他の設定は不要だ。
 そして、conceal.phpにこのページを登録フォームとして取り扱うルーティングを設定する。

add_filter( 'register_url', function( $url ) {
    return home_url( 'signup/' );
}, 10 );
add_action( 'login_form_register', function() {
    $http_post = ( 'POST' === $_SERVER['REQUEST_METHOD'] );
    if ( $http_post ) {
        if ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) {
            $user_login = $_POST['user_login'];
        }
        if ( isset( $_POST['user_email'] ) && is_string( $_POST['user_email'] ) ) {
            $user_email = wp_unslash( $_POST['user_email'] );
        }
        // Execute new user registration
        $errors = register_new_user( $user_login, $user_email );
        if ( ! is_wp_error( $errors ) ) {
            global $errors;
            $errors = new WP_Error( 'registered' );
            $redirect_to = home_url( 'signin/?checkemail=registered' );
        } else {
            $query_str = '';
            if ( $errors->has_errors() ) {
                $code_hash = array_map( function( $_code ){ return hash( 'crc32b', $_code ); }, $errors->get_error_codes() );
                $query_str = '?'. http_build_query( [ 'cd' => implode( ',', $code_hash ) ] );
            }
            $redirect_to = home_url( 'signup/'. $query_str );
            header( 'Location: '. $redirect_to, true, 307 );
            exit;
        }
    }
    wp_safe_redirect( $redirect_to );
    exit;
});

 ユーザー登録時、認証メールが登録したメールアドレスに送信され、そのメール内に記載されているURLからログイン用のパスワード設定を行うルーティングになっている。なので、メール内のURLも下記のようにフィルターしておこう。

add_filter( 'wp_new_user_notification_email', function( $wp_new_user_notification_email, $user, $blogname ) {
    return str_replace( [ WP_INSTALL_DIR . 'wp-login.php', 'wp-login.php' ], LOGIN_PAGE_FILE, $wp_new_user_notification_email );
}, 10, 3 );

 次に、ユーザー登録後の認証メールからアクセスするパスワード設定周りを作っていく。パスワード設定用のテンプレートはpartials/rp-form.phpであり、この中身は次のようにする。

<?php
/*
Template Name: Reset Password
*/
if ( isset( $_COOKIE[ 'rp-' . COOKIEHASH ] ) && 0 < strpos( $_COOKIE[ 'rp-' . COOKIEHASH ], ':' ) ) {
    list( $rp_login, $rp_key, $is_set_new_passwd ) = explode( ':', wp_unslash( $_COOKIE[ 'rp-' . COOKIEHASH ] ), 3 );
}
$is_set_new_passwd = (bool) $is_set_new_passwd;
$is_invalid_access = ( empty( $rp_login ) || empty( $rp_key ) );
$page_title = $is_set_new_passwd ? __( 'Set New Password' ) : __( 'Reset Password' );
if ( isset( $_REQUEST['password'] ) && 'reseted' === $_REQUEST['password'] ) {
    $page_title = __( 'New password has been set' );
} else
if ( $is_invalid_access ) {
    global $errors;
    $page_title = __( 'Invalid Access' );
    $errors = new WP_Error( 'invalid_access' );
}
$submit_label = $is_set_new_passwd ? __( 'Set Password' ) : __( 'Reset Password' );
$use_pass_strength = false;
if ( $use_pass_strength ) {
    wp_enqueue_script( 'utils' );
    wp_enqueue_script( 'user-profile' );
}

get_header();
?>
<main id="site-content" role="main">
    <div class="entry-content" style="padding-top: 5rem; height: calc(100vh - 250px);">
        <h2 class="has-text-align-center"><?= $page_title ?></h2>
        <?php get_template_part( 'partials/notices' ); ?>
<?php if ( ! $is_invalid_access && ( ! isset( $_REQUEST['password'] ) || 'reseted' !== $_REQUEST['password'] ) ) : ?>
        <form name="resetpassform" id="resetpassform" action="<?= esc_url( network_site_url( 'wp-login.php?action=resetpass', 'login_post' ) ); ?>" method="post" autocomplete="off">
            <input type="hidden" id="user_login" value="<?= esc_attr( $rp_login ); ?>" autocomplete="off">
            <?php if ( $use_pass_strength ) : ?><div class="user-pass1-wrap"><?php endif; ?>
                <p>
                    <label for="pass1"><?= __( 'New password' ); ?></label>
<?php if ( ! $use_pass_strength ) : ?>
                    <input type="password" name="pass1" id="pass1" class="input" size="20" value="" autocomplete="off">
<?php endif; ?>
                </p>
<?php if ( $use_pass_strength ) : ?>
                <div class="wp-pwd">
                    <div style="display: flex; flex-direction: row;">
                        <input type="password" data-reveal="1" data-pw="<?= esc_attr( wp_generate_password( 16 ) ); ?>" name="pass1" id="pass1" class="input password-input" size="24" value="" autocomplete="off" aria-describedby="pass-strength-result">
                        <button type="button" class="button button-secondary wp-hide-pw hide-if-no-js" data-toggle="0" aria-label="<?= esc_attr__( 'Hide password' ); ?>">
                            <?= esc_attr__( 'Hide password' ); ?>
                        </button>
                    </div>
                    <div id="pass-strength-result" class="hide-if-no-js" aria-live="polite"><?= __( 'Strength indicator' ); ?></div>
                </div>
                <div class="pw-weak">
                    <input type="checkbox" name="pw_weak" id="pw-weak" class="pw-checkbox">
                    <label for="pw-weak"><?= __( 'Confirm use of weak password' ); ?></label>
                </div>
            </div>
            <p class="user-pass2-wrap">
<?php else : ?>
            <p>
<?php endif; ?>
                <label for="pass2"><?= __( 'Confirm new password' ); ?></label>
                <input type="password" name="pass2" id="pass2" class="input" size="20" value="" autocomplete="off">
            </p>
            <p class="description indicator-hint"><?= wp_get_password_hint(); ?></p>
            <br>
            <input type="hidden" name="rp_key" value="<?= esc_attr( $rp_key ); ?>">
<?php if ( $is_set_new_passwd ) : ?>
            <input type="hidden" name="new_passwd" value="1">
<?php endif; ?>
            <p class="submit">
                <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?= esc_attr( $submit_label ); ?>">
            </p>
        </form>
        <p id="nav" style="text-align: right; font-size: 1.6rem;">
            <a href="<?= esc_url( wp_login_url() ); ?>"><?= __( 'Sing in' ); ?></a>
<?php if ( get_option( 'users_can_register' ) && ! $is_set_new_passwd ) : ?>
            <span>|</span>
            <a href="<?= esc_url( wp_registration_url() ); ?>"><?= __( 'Register' ); ?></a>
<?php endif; ?>
        </p>
<?php endif; ?>
    </div>
</main>
<?php
get_footer();

 パスワード脆弱性検証器を利用する場合は、変数$use_pass_strength = true;とフラグを切り替える。しかし、この機能は依存系のJavaScriptの読み込みにおいて、前章で施策した秘匿化が一部効かないので、別途スクリプトのパスをフィルターしてやる必要がある。面倒なので、今回の例では使用しないようにしてある。

 次に、このテンプレートを使用した固定ページを作成する。タイトルを「リセットパスワード」、URLスラッグをresetpass、ページ属性のテンプレートを「Reset Password」とする。本文など他の設定は不要だ。
 そして、conceal.phpにこのページを取り扱うルーティングを設定する。

add_action( 'validate_password_reset', function( $errors, $user ) {
    $http_post = ( 'POST' === $_SERVER['REQUEST_METHOD'] );
    list( $rp_path ) = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) );
    $rp_cookie       = 'wp-resetpass-' . COOKIEHASH;
    if ( ! $errors->has_errors() && isset( $_POST['pass1'] ) && ! empty( $_POST['pass1'] ) ) {
        reset_password( $user, $_POST['pass1'] );
        setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true );
        setcookie( 'rp-' . COOKIEHASH, ' ', time() - YEAR_IN_SECONDS, 'resetpass/', COOKIE_DOMAIN, is_ssl(), true );
        $redirect_to = home_url( 'resetpass/?password=reseted' );
    } else {
        $is_set_new_passwd = ( 'rp' === $_GET['action'] || isset( $_POST['new_passwd'] ) );
        if ( isset( $_COOKIE[ $rp_cookie ] ) && 0 < strpos( $_COOKIE[ $rp_cookie ], ':' ) ) {
            $value = $_COOKIE[ $rp_cookie ] .':'. ( $is_set_new_passwd ? 1 : 0 );
        }
        setcookie( 'rp-' . COOKIEHASH, $value, 0, 'resetpass/', COOKIE_DOMAIN, is_ssl(), true );
        if ( $http_post ) {
            if ( ! isset( $_POST['pass1'] ) || empty( $_POST['pass1'] ) ) {
                $errors->add( 'empty_password', '' );
            }
            if ( ! isset( $_POST['pass2'] ) || empty( $_POST['pass2'] ) ) {
                $errors->add( 'empty_confirm_password', '' );
            }
        }
        $code_hash = array_map( function( $_code ){ return hash( 'crc32b', $_code ); }, $errors->get_error_codes() );
        $query_str = ! empty( $code_hash ) ? '?'. http_build_query( [ 'cd' => implode( ',', $code_hash ) ] ) : '';
        $redirect_to = home_url( 'resetpass/'. $query_str );
    }
    wp_safe_redirect( $redirect_to );
    exit;
}, 10, 2 );

 このパスワードリセット関連の仕組みは、次に説明するパスワード紛失時のパスワード再設定でも利用される。

パスワード紛失フォームをテンプレートから読み込む

 最後はパスワード紛失時のリマインダ周りを作成する。やり方は今までのフォームページと同等で、パスワード紛失フォーム用のテンプレートpartials/lostpass-form.phpを下記のように更新する。

<?php
/*
Template Name: Lostpassword
*/
get_header();
?>
<main id="site-content" role="main">
    <div class="entry-content" style="padding-top: 5rem; height: calc(100vh - 250px);">
        <h2 class="has-text-align-center"><?= __( 'Lost Password' ) ?></h2>
        <?php get_template_part( 'partials/notices' ); ?>
        <form name="lostpasswordform" id="lostpasswordform" action="<?= esc_url( network_site_url( 'wp-login.php?action=lostpassword', 'login_post' ) ); ?>" method="post">
            <p>
                <label for="user_login"><?php _e( 'Username or Email Address' ); ?></label>
                <input type="text" name="user_login" id="user_login" class="input" value="<?= esc_attr( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ? wp_unslash( $_POST['user_login'] ) : '' ); ?>" size="20" autocapitalize="off">
            </p>
            <input type="hidden" name="redirect_to" value="<?= esc_attr( isset( $_REQUEST['redirect_to'] ) && ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '' ); ?>" />
            <p class="submit">
                <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?= esc_attr__( 'Get New Password' ); ?>">
            </p>
        </form>
        <p id="nav" style="text-align: right; font-size: 1.6rem;">
            <a href="<?= esc_url( wp_login_url() ); ?>"><?= __( 'Sing in' ); ?></a>
<?php if ( get_option( 'users_can_register' ) ) : ?>
            <span>|</span>
            <a href="<?= esc_url( wp_registration_url() ); ?>"><?= __( 'Register' ) ?></a>
<?php endif; ?>
        </p>
    </div>
</main>
<?php
get_footer();

 次に、このテンプレートを使用した固定ページを作成する。タイトルを「パスワード紛失」、URLスラッグをlostpass、ページ属性のテンプレートを「Lostpassword」とする。本文など他の設定は不要だ。
 そして、conceal.phpにこのページを取り扱うルーティングを設定する。

add_filter( 'lostpassword_url', function( $url ) {
    $url = home_url( 'lostpass/' );
    return $url;
}, 10 );
add_filter( 'wp_redirect', function( $location, $status ) {
    if ( strpos( $location, 'checkemail=confirm' ) !== false && strpos( $_SERVER['REQUEST_URI'], 'action=lostpassword' ) !== false ) {
        $location = str_replace( [ WP_INSTALL_DIR . 'wp-login.php', 'wp-login.php' ], 'lostpass', $location );
    }
    return $location;
}, 10, 2 );
add_action( 'lost_password', function( $errors ) {
    $redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '';
    $error_codes = [];
    if ( $errors->has_errors() ) {
        foreach( $errors->get_error_codes() as $_code ) {
            if ( 'empty_username' === $_code ) {
                $error_codes[] = 'emptycombo';
            } else
            if ( 'invalid_email' === $_code ) {
                $error_codes[] = 'forced_completion';
            } else {
                $error_codes[] = $_code;
            }
        }
        if ( in_array( 'forced_completion', $error_codes, true ) ) {
            $redirect_to = home_url( 'lostpass/?checkemail=confirm' );
        } else {
            $code_hash = array_map( function( $_code ){ return short_hash( $_code ); }, $error_codes );
            $query_str = ! empty( $code_hash ) ? '?'. http_build_query( [ 'cd' => implode( ',', $code_hash ) ] ) : '';
            $redirect_to = home_url( 'lostpass/'. $query_str );
        }
    }
    wp_safe_redirect( $redirect_to );
    exit;
}, 10 );

 パスワード紛失時はフォームからサブミットされると、確認メールが登録メールアドレスに送信され、そこからパスワード再設定用フォーム(ユーザ登録で作成したパスワードリセットの流れ)へルーティングされる。そのため、確認メールに記載されているURLもフィルターする必要があるので、下記のフィルターフックを追加しておこう。

add_filter( 'retrieve_password_message', function( $message, $key, $user_login, $user_data ) {
    return str_replace( [ WP_INSTALL_DIR . 'wp-login.php', 'wp-login.php' ], LOGIN_PAGE_FILE, $message );
}, 10, 4 );

 これで完了だ。いやはや、結構なボリュームの施策になった。WordPressの認証周りのカスタマイズ&秘匿化は骨が折れるが、ここをおろそかにするとサイトの秘匿化はおよそ達成できないので気合を入れてやることが重要だ。

Cookieの秘匿化

 認証周りの秘匿化でできればやりたいのが、WordPressが発行しているCookieからWordPressの臭いを消すことだ。現状、プラグインやテーマ等で発行されるCookieを除くWordPress自体が発行するCookieは下記の通りだ。

Cookie名 期間 用途 対象 発行場所
wordpress_test_cookie セッション サイトにアクセスしているブラウザがCookieを許可しているかどうかを確認する すべてのユーザー ログイン画面(wp-login.php)
wp-settings(-time|)-{user_id} 1年 ユーザの管理画面設定を保持する。基本的に管理画面のビューをカスタマイズするために使われるが、場合によってはメインサイトでも使われる ログイン中のユーザーのみ 管理画面(/wp-admin/)
wordpress_logged_in_{hash} セッション(ログイン時に「ログイン状態を保存」を有効にした場合は14日) ほとんどのインターフェースにてログインした時刻やユーザアカウント自体を示す ログイン中のユーザーのみ ログイン画面(wp-login.php)
wordpress_sec_{hash} セッション(ログイン時に「ログイン状態を保存」を有効にした場合は14日) 管理画面用の認証Cookieで、用途は wordpress_logged_in_{hash} と同等 ログイン中のユーザーのみ 管理画面(/wp-admin/)
wp-resetpass-{hash} ワンタイム パスワード再設定時のリダイレクション処理に使用される。リダイレクション後に即破棄される 認証メールからアクセス時 ログイン画面(wp-login.php)

 サイトからWordPressの存在を隠蔽するにあたっては、これらのCookie名から「wordpress」や「wp」といったキーワードを排除しておきたい。これらのCookie名はWordPressの定数としてwp-includes/default-constants.phpで定義されているので、このファイルが読み込まれる前に定義することで、書き換えることが可能だ。実際にはwp-config.phpで定義することになるだろう。第一章でwp-config.phpWP_SITEURL定数を定義してあるので、その定義より後ろに下記のような設定を追加しよう。

/* Cookie Settings */
define( 'COOKIEHASH', md5( WP_SITEURL ) );
define( 'USER_COOKIE', 'mysite_user_' . COOKIEHASH );
define( 'PASS_COOKIE', 'mysite_pass_' . COOKIEHASH );
define( 'AUTH_COOKIE', 'mysite_' . COOKIEHASH );
define( 'SECURE_AUTH_COOKIE', 'mysite_sec_' . COOKIEHASH );
define( 'LOGGED_IN_COOKIE', 'mysite_logged_in_' . COOKIEHASH );
define( 'TEST_COOKIE', 'mysite_test_cookie' );
define( 'RECOVERY_MODE_COOKIE', 'mysite_rec_' . COOKIEHASH );

 この状態で一旦WebサイトのCookieを削除してから、WordPressにログインし直してみると、発行される認証Cookie名からWordPressのキーワードが排除される。しかし、管理画面の設定保存用のCookie wp-settings-time-{user_id} はまだ残っている。しかし、この Cookie は wp-includes/option.php の関数wp_user_settings() 内にハードコーディングされてしまっていて、変更するにはコアのソースコードを修正する必要がある。WordPressの可用性を上げる意味でもフィルターフックを追加するとか、他のCookieと同様に定数化して欲しいものだが、現状で手を出すのは難しい部分だ(コアソースを修正してしまうと、WordPressの更新時に都度手を入れなければならないため)。一応、これについては Core Trac にも3年前から改善要望としてチケットが上がっているのだが、まだ未対応である(なので、私も切望コメントを書いておいた)。
 また、パスワード再設定用の wp-resetpass-{hash} のCookie名も wp-login.php 内にハードコーディングされているので、ソースを直接修正しない限りは変更ができない。ただし、こちらはワンタイムCookieで、このCookieが発行された後のリダイレクト直後に破棄されてしまうので、サイト隠蔽化に影響しないものと認識して構わないだろう。
 それ以外のプラグイン等で発行されているCookieは個別にチェックして、必要があれば隠蔽化していくことになる。

以上で、登録・認証周りの秘匿化は完了だ。いやはや、だいぶ骨が折れた。