前回の記事では、wp-admin内のREST APIリクエストがCookieとnonceの組み合わせで守られている仕組みを扱った。今回はその前提となる、「ログインする」という操作そのものが裏側で何をしているのかを掘り下げる。パスワードを一度入力しただけで、その後は何十ページ遷移してもログイン状態が保たれるのはなぜか、そしてWordPressのこの仕組みは一般的な「セッション認証」とどう違うのか、という2点を整理する。
ログインボタンを押した瞬間に何が起きるか
wp-adminのログインフォームでユーザー名とパスワードを送信すると、サーバー側では wp_signon() という関数がまず入力されたパスワードとデータベースに保存されているハッシュ値を照合する。ここまでは一般的なログイン処理と変わらない。認証が成功すると、wp_set_auth_cookie() が呼ばれ、ブラウザに認証Cookieが発行される。以降のリクエストでは、ブラウザがこのCookieを自動的に付けて送ってくるため、ユーザーは毎回パスワードを入力し直す必要がなくなる。
補足: HTTPそのものには「状態」という概念がない。1つ前のリクエストで誰がログインしたかを、サーバーは次のリクエストの時点では何も覚えていない。この「状態のないプロトコルの上でログイン状態を維持する」という課題を解決する手段の総称が、一般に「セッション管理」と呼ばれる。
認証Cookieの中身は何でできているか
WordPressの認証Cookie(wordpress_logged_in_* という名前で保存される)は、中身を見るとパイプ区切りの文字列になっている。おおまかには「ユーザー名」「Cookieの有効期限」「セッショントークン」「HMAC署名」の4つの要素で構成されている。ここで重要なのは、Cookie自体にユーザー名や有効期限がそのまま平文に近い形で入っているという点だ。
補足: HMAC(Hash-based Message Authentication Code)とは、秘密鍵を使って生成する改ざん検知用の署名値のこと。同じ入力とキーからは必ず同じ値が得られるが、キーを知らない第三者は正しい値を偽造できない。
もしCookieの中身をユーザー自身が書き換えられてしまえば、他人のユーザー名に差し替えてなりすますことができてしまう。これを防いでいるのが末尾のHMAC署名だ。サーバーは wp-config.php に定義された AUTH_KEY / AUTH_SALT(およびログイン専用の LOGGED_IN_KEY / LOGGED_IN_SALT)という秘密の文字列を使ってこの署名を計算しており、Cookieを受け取るたびに同じ計算をやり直して値が一致するかを検証する。攻撃者はこの秘密鍵を知らない限り、ユーザー名や有効期限を書き換えても正しい署名を作れないため、改ざんはその場で検出される。
毎リクエストで行われている検証
ページを開くたびに、WordPressは wp_validate_auth_cookie() でこの検証を実行している。流れとしては、①Cookieをパイプ区切りで分解する、②有効期限が切れていないか確認する、③ユーザー名とトークンと有効期限から期待されるHMAC値を計算し直す、④Cookie内の署名値と一致するかを比較する、という順番になる。1つでも食い違えば、そのリクエストは未ログイン扱いとして弾かれる。
「ステートレスなのに全ログアウトができる」という一見矛盾した仕組み
ここまでの説明だけを見ると、WordPressの認証はサーバー側に何も記録を持たない「自己完結型」の方式に見える。実際、ユーザー名・有効期限・署名さえ検証できればよいので、サーバー側がリクエストのたびにデータベースを引く必要はない。この設計は、サーバー側に大量のセッションデータを保持しなくて済むという利点がある。
ところがWordPressには「すべての端末からログアウトする」という管理画面の機能がある。もしCookieが本当に完全に自己完結していたら、発行済みの正規のCookieを外部から無効化する手段がないはずだ。この矛盾を埋めているのが、Cookieの中に含まれる「セッショントークン」の部分だ。WordPressはユーザーごとに、現在有効なセッショントークンの一覧をデータベースのユーザーメタ情報として別途保持している(WP_Session_Tokens というクラスが担当している処理)。認証Cookieの検証時には、HMAC署名の一致に加えて、Cookie内のトークンがこの有効トークン一覧に含まれているかどうかも確認される。「全ログアウト」を実行すると、このユーザーメタ側のトークン一覧をサーバー側でまとめて削除するため、署名自体は正しくても紐づくトークンが見つからなくなり、以降のリクエストは認証切れとして扱われる。
つまりWordPressの認証は、純粋な自己完結型トークンと、サーバー側で管理する失効可能なセッション情報のハイブリッド構成になっている。署名検証の部分は自己完結的に高速に処理しつつ、失効管理が必要な部分だけをサーバー側の記録に頼ることで、両方の利点を両立させている。
一般的な「サーバー側セッション」方式との違い
多くのWebアプリケーションで採用される伝統的なセッション管理は、これとは逆の設計を取ることが多い。ブラウザに渡すCookieの中身は意味を持たないランダムな文字列(セッションID)だけにしておき、ユーザー名や権限といった実際の情報はすべてサーバー側のメモリやデータベースに「セッションID → ユーザー情報」という対応表として保存する方式だ。この場合、サーバーは毎リクエストごとにこの対応表を引く必要があるが、失効させたいときはその対応表からエントリを1件消すだけで即座に無効化できる。
WordPressの認証Cookieは、この2つの設計の中間に位置していると捉えると理解しやすい。ユーザー名や有効期限といった軽量な情報はCookie自体に署名付きで持たせて検証コストを抑えつつ、「今どのトークンが有効か」という失効管理が必要な部分だけを、ユーザーメタというサーバー側の記録に切り出している。
まとめ
wp-adminのログイン状態は、ユーザー名・有効期限・トークン・HMAC署名の4要素からなる認証Cookieによって、リクエストのたびに再検証されている。署名の検証だけを見ると自己完結型のトークン方式に見えるが、実際にはユーザーメタに保持されたセッショントークン一覧と組み合わせたハイブリッド構成になっており、これによって「全ログアウト」のようなサーバー側からの能動的な失効操作が可能になっている。前回のREST API認証の記事で扱ったnonce検証も、このログインCookieが正しく機能していることを前提にした仕組みであり、両者は一段深いところで同じ認証基盤の上に成り立っている。