/wp-json/ から始まるWordPressのREST API(以前の記事で基礎を扱った)には、認証なしでも読み取れるエンドポイントと、ログインしていないと弾かれるエンドポイントの両方が混在している。wp-admin管理画面の中でブラウザが裏側で投げているリクエストは、ユーザーが毎回パスワードを入力し直すことなく「今ログイン中の自分」として認証済みで通っている。今回は、この仕組みを支えているCookie+nonce認証と、それとは別の目的で用意されているApplication Passwordsという2つの認証方式が、それぞれ何を守っていて、どういう場面で使い分けられているのかを整理する。
wp-adminの中のリクエストは何で認証されているか
WordPressにログインすると、ブラウザには wordpress_logged_in_* という名前の認証Cookieが発行される。以降、同じブラウザから wpmm.jp 宛てに送られるリクエストにはこのCookieが自動的に付いてくるため、サーバー側はこのCookieを検証するだけで「誰がログイン中か」を判定できる。wp-admin内のJavaScriptがREST APIを呼ぶとき(投稿の自動保存や、ブロックエディタが下書きを更新するときなど)も、この認証Cookieに乗って送られている。
補足: CSRF(Cross-Site Request Forgery)とは、ログイン中のユーザーが悪意あるサイトを開いてしまった際に、そのサイトが勝手にユーザーのCookieを使って正規サイトへリクエストを送りつけ、意図しない操作(投稿削除・設定変更等)を実行させてしまう攻撃のこと。ブラウザはCookieを自動送信するため、Cookieだけでは「本人が本当にこの操作を意図したか」までは分からない。
ここで問題になるのがCSRFだ。Cookieはブラウザが自動的に付けてくれる分、悪意あるサイトを開いてしまった場合にも同じCookieが乗った状態でリクエストが送られてしまう。もしCookieの検証だけでWordPressのREST APIが書き込みリクエストを受け付けてしまうなら、ログイン中に細工されたページを踏んだだけで、意図しない投稿削除やユーザー情報変更が実行されかねない。
nonceが埋める隙間
この隙間を埋めているのがnonce(ワンタイムトークンに近い性質を持つ検証用文字列)だ。WordPressのnonceは厳密には「一度きり」ではなく、ログインセッションと紐づいた上で一定時間(既定で24時間、ローテーションを含めると最大48時間程度)有効な、操作の種類ごとに異なる値を持つトークンとして実装されている。サーバー側が wp_create_nonce('特定のアクション名') で発行し、wp-admin画面のHTML/JSに埋め込んでおく。REST APIへの書き込みリクエストにはこのnonceを X-WP-Nonce ヘッダーに乗せて送る決まりになっており、サーバー側は wp_verify_nonce() でCookieのユーザーとnonceの組み合わせが正しいかを検証する。
外部の悪意あるサイトはこのnonceの値を知る手段がない(同一オリジンのwp-admin画面にしか埋め込まれていないため)ので、Cookieだけを盗用してもnonceの検証で弾かれる。つまりCookie認証は「誰か」を、nonce検証は「その人が今このページから意図して操作したか」を、それぞれ別の観点でチェックしている。両方が揃って初めてwp-admin内部のREST APIリクエストが通る、という二段構えになっている。
Cookie+nonceが向いていない場面
この仕組みは、あくまで「同一ブラウザのwp-admin画面から、同じセッション内で」リクエストを送る前提で設計されている。裏を返せば、ブラウザを介さない外部プログラム(コマンドラインツールや、別サーバーで動くスクリプト)からREST APIを呼びたい場合には向いていない。Cookieはブラウザのログインセッションに紐づくものであり、外部プログラムがそれを取得・維持する経路が自然には存在しないし、nonceも短命でwp-admin画面をロードするたびに再取得が必要になる。
Application Passwordsという別の入口
WordPress 5.6(2020年公開)からコアに搭載されたApplication Passwordsは、この「外部プログラムから安全にREST APIを叩きたい」という要求に応えるための、Cookie+nonceとは別系統の認証方式だ。仕組みとしてはHTTP Basic認証に近く、ユーザーはwp-adminのプロフィール画面で「アプリ名」を指定してApplication Passwordを新規発行できる。発行されるのはログインパスワードとは別の、ランダム生成された専用パスワードで、これをリクエストの Authorization: Basic ヘッダーに載せて送るとログインパスワードと同等の権限でAPIが実行される。
Cookie+nonceとの決定的な違いは2点ある。1つは「ブラウザのセッション状態に依存しない」こと(発行したパスワードさえあれば、どのプログラムからでも即座に使える)。もう1つは「アプリ単位で個別に無効化できる」ことだ。ログインパスワード自体を外部連携ツールに渡す必要がなく、そのツールとの連携をやめたくなったら、そのApplication Passwordだけをwp-admin側から失効させればよい。ログインパスワード本体を変更する必要がないため、他の連携先に影響を与えずに1本だけ切り離せる。
実例で見る「認証が要らない」ケース
実は、当ブログのLP(wpmm.jp/)トップに出している「最新の記事」セクション(前回の記事で扱った blog_latest.php)は、この2つの認証方式のどちらも使っていない。ここが呼んでいるのは /wp/v2/posts という、公開済み投稿を取得するだけの読み取り専用エンドポイントで、そもそも認証なしでも誰でもアクセスできる設計になっているからだ。REST APIの認証が必要になるのは、下書きの取得・投稿の作成や更新・設定変更のような、公開情報の範囲を超える操作をする場合に限られる。「読むだけなら誰でも」「書くならログイン中の自分だけ」「外部プログラムから書くなら専用のApplication Password」という3段階に分けて考えると、REST APIのどの認証方式を選ぶべきかを迷わず判断できるようになる。
まとめ
wp-admin内部のREST APIリクエストは、Cookie(誰がログイン中か)とnonce(その人が今このページから意図した操作か)という2つの検証を組み合わせることで、CSRFを防ぎながら「毎回パスワード入力なし」の利便性を両立させている。一方でこの仕組みはブラウザのセッションに強く依存するため、外部プログラムからREST APIを呼びたい場合にはApplication Passwordsという、ログインパスワードとは切り離して発行・失効できる別系統のトークンが用意されている。同じ「REST APIの認証」という言葉でも、リクエストの発生元がブラウザの中か外かによって適した方式が変わる、という点を押さえておくと設計判断がしやすい。