WordPress・CMS設計

WordPress REST APIの認証方式:個人開発で選ぶべき安全な接続手段

WordPressをヘッドレスCMSとして運用しようと決めた日の夜、外部のLaravelから記事データを一発で取得したくなって、意気揚々と/wp-json/wp/v2/postsにGETを飛ばした。公開済みの記事なら、そのリクエストは通常、認証なしでも通る。ところが、非公開記事を取得したり、記事の更新やメディアの登録を試したりすると、今度は401が返ってくる。…

WordPress REST APIの認証方式:個人開発で選ぶべき安全な接続手段

エンドポイントは存在する。URLも間違っていない。それでも、権限が必要なデータや書き込み操作は、認証と認可がなければ処理できない。ここで初めて、WordPress REST APIの認証方式を選ぶ必要が出てくる。

公式ドキュメントを開けば、認証方式はいくつか紹介されている。アプリケーションパスワード、CookieとNonce、JWT、OAuth 2.0。どれも検索すれば実装例が出てくるが、選択肢が多いほど、最初の設計で迷う。特に個人開発では、認証そのものより、採用した方式を半年後も安全に更新できるかどうかのほうが重要になる。

先に結論を言えば、WordPress 5.6以降を使っていて、Laravelやバッチ、別サーバーのバックエンドからREST APIを呼び出すなら、まず検討すべきはアプリケーションパスワードだ。WordPressコアに統合された標準機能であり、用途ごとに資格情報を分けて管理できる。

ただし、ブラウザ上のJavaScriptから別オリジンのWordPressを操作する場合は、事情が変わる。同一オリジンならCookieとNonce、クロスオリジンのSPAやモバイルアプリならJWTなど、通信する場所とユーザーの扱いによって適切な方式は変わる。

認証方式を名前だけで選ぶのではなく、誰が、どこから、どのエンドポイントを、どの権限で呼ぶのかを先に決める。ここを飛ばすと、あとからCORSやトークンを継ぎ足すことになり、構成の安全性を説明できなくなる。

WordPress REST APIの認証方式4つと、それぞれの位置づけ

WordPress REST APIで検討される認証方式は、実務上、次の4つに大別できる。

認証方式向いている通信必要なもの注意点
アプリケーションパスワードLaravel、CLI、バッチ、別サーバーWordPress 5.6以降、原則HTTPSBasic形式で送信するため通信経路の保護が必須
Cookie+NonceWordPress内のテーマ、管理画面、同一オリジンのJavaScriptログインセッション、NonceCookieだけではCSRF対策にならない
JWT別オリジンのSPA、モバイルアプリJWT対応プラグインなどトークンの有効期限や失効処理を設計する必要がある
OAuth 2.0第三者アプリへの権限委譲OAuth対応の仕組み認可サーバーやリダイレクトなど、構成が重くなりやすい

ここで整理しておきたいのは、Basic認証、JWT、OAuth 2.0を同じ種類の選択肢として横一列に並べてはいけないということだ。

アプリケーションパスワードは、HTTPのBasic形式を使ってWordPressのユーザー認証を行うためのコア機能である。Basic認証そのものをWordPressで使うには必ずプラグインが必要、という話ではない。WordPress 5.6以降なら、アプリケーションパスワードが標準でBasic形式の認証情報を発行する。

一方、JWTは認証後に渡すトークンの形式、あるいはそのトークンを使った認証方式として扱われる。OAuth 2.0は、第三者のアプリケーションに対して、どの権限をどの範囲で委譲するかを定める認可の枠組みだ。

JWTがOAuth 2.0の上位互換という関係ではない。OAuth 2.0のアクセストークンにJWT形式が使われることはあるが、OAuth 2.0とJWTは役割が違う。

この違いを曖昧にしたまま、「新しい認証だからJWT」「本格的な連携だからOAuth」と進めると、必要以上に複雑な仕組みを抱えることになる。個人開発では、複雑さそのものが脆弱性になる。設定を忘れ、更新を止め、失効手段を用意しないまま本番に出すからだ。

アプリケーションパスワードの仕組み:標準機能で使えるBasic形式の認証

アプリケーションパスワードは、WordPress 5.6でコアに統合された機能だ。管理画面のユーザープロフィールから、通常のログインパスワードとは別に、外部アプリケーション用のパスワードを発行できる。

通信では、ユーザー名とアプリケーションパスワードを組み合わせ、HTTPのAuthorizationヘッダーにBasic形式で設定する。概念的には、次のような認証情報を送る。

Authorization: Basic <ユーザー名とパスワードを結合してBase64エンコードした値>

Base64は暗号化ではない。ここは何度でも強調しておきたい。Base64に変換しただけの文字列は、復元しようと思えば簡単に復元できる。したがって、アプリケーションパスワードの安全性は、Base64ではなくHTTPSによる通信路の暗号化に依存している。

管理画面では、1人のユーザーに対して複数のアプリケーションパスワードを発行できる。Laravel連携用、定期実行スクリプト用、検証環境用というように用途を分けておけば、不要になった資格情報だけを取り消せる。

これは個人開発でかなり重要な性質だ。すべての外部連携で同じ管理者パスワードを使っていると、どれか1つのサービスから漏洩した時点で、全連携を止めて作り直すことになる。用途別に分けておけば、影響範囲を限定できる。

アプリケーションパスワードは、単に「パスワードをもう1つ作る」機能ではない。連携先ごとに資格情報を分け、不要になった接続だけを切るための仕組みだ。

アプリケーションパスワードが向いている通信

アプリケーションパスワードが扱いやすいのは、WordPressの外側にあるバックエンドやスクリプトからAPIを呼び出すケースだ。

  • LaravelからWordPressの記事を取得・更新する
  • CLIやバッチで投稿を登録する
  • 定期処理からメディアやカスタム投稿を操作する
  • サーバー間のWebhook連携を行う
  • 管理者がブラウザで直接操作するのではなく、固定されたバックエンドがAPIを呼ぶ

この場合、認証情報をブラウザに渡す必要がない。Laravelの環境変数やシークレット管理機能に保存し、サーバー側からだけ送信する構成にできる。プラグインを追加せず、WordPressコアの機能だけで運用を始められるのも大きい。

ただし、アプリケーションパスワードを管理者権限のユーザーで発行する場合、その資格情報を使った処理は、そのユーザーの権限で実行される。記事の取得だけが必要なのに、サイト全体を変更できるユーザーの資格情報を渡すのは、権限設計として雑だ。

可能であれば連携専用のユーザーを用意し、必要な権限だけを持たせる。WordPress標準の権限だけでは細かく分けにくい場合でも、少なくとも個人の管理者アカウントをそのまま外部システムに渡す構成は避けたい。

公開データと保護すべきデータを分ける

認証の設計では、APIを使うものはすべて認証が必要だと考えないことも重要だ。

公開済みの投稿を取得するGETリクエストは、通常、認証なしで利用できる。ヘッドレスCMSの公開ページが、WordPressの公開APIから記事一覧を読むだけなら、そこにログイン情報やJWTを埋め込む必要はない。

一方で、非公開記事、下書き、予約投稿、ユーザー情報などは、公開用の取得とは別に扱う必要がある。投稿の作成・更新・削除、メディアのアップロード、設定値の変更も同様だ。これらは、認証されたユーザーが必要な権限を持っているかを確認してから処理しなければならない。

公開データの読み取りまで認証対象にすると、フロントエンドへ不要な資格情報を渡すことになる。反対に、書き込み操作を公開エンドポイントの延長として扱えば、サイトの状態を変更できる入口を広げることになる。

「どの通信に認証が必要か」をエンドポイント単位で切り分けることが、方式選びの前提になる。

HTTPSが必要な理由と、ローカル環境での例外

アプリケーションパスワードは、HTTPヘッダーに認証情報を載せて送信する。通信が暗号化されていなければ、ネットワーク上で内容を盗み見られた場合に、ユーザー名とパスワードを復元される可能性がある。

本番環境でhttp://のまま使うのは、認証方式の問題というより、通信経路の設計ミスだ。ログインフォームだけHTTPSにすればよいわけではない。REST APIへのリクエストも含め、認証情報が通過する経路全体をHTTPSで保護する必要がある。

一方で、「アプリケーションパスワードはHTTPSでなければ絶対に動かない」と断定するのも正確ではない。WordPressは、開発環境のlocalhostなど、ローカル利用を想定した例外を考慮している。Dockerやローカルサーバーでhttp://localhostを使っている場合、アプリケーションパスワードを利用できることがある。

また、サイトやサーバーの構成によっては、HTTPSで運用しているにもかかわらず、WordPressが安全な接続として認識できず、アプリケーションパスワードの項目が表示されないことがある。リバースプロキシやロードバランサーの背後にWordPressを置いている場合、プロキシからHTTPSで届いたことをWordPress側が正しく認識できているか確認したい。

実務上の整理はこうなる。

  • 本番環境ではHTTPSを必須条件として扱う
  • localhostなどのローカル開発環境ではHTTPが許容される場合がある
  • ステージング環境でも、可能なら本番に近いHTTPS構成にする
  • HTTPSなのに発行画面が出ない場合は、サイトURLだけでなくプロキシやサーバーの認識も確認する

開発環境で動いたことと、本番環境で安全に運用できることは別だ。ローカルのHTTPをそのまま本番に持ち込まない。ここは認証方式を比較する以前の、Webサービス運営の前提条件である。

Basic形式のヘッダーでつまずきやすい点

アプリケーションパスワードは、発行画面では読みやすいように区切って表示される。表示上の空白をどう扱うかは、使用するクライアントやライブラリの実装に左右されるため、WordPressの公式ドキュメントや利用ライブラリの仕様に合わせるのが安全だ。

少なくとも、パスワードを通常のログインパスワードと混同しないこと、発行直後に値を安全な場所へ保存すること、画面を閉じた後に再表示できるとは限らないことは覚えておきたい。

Laravelなどのバックエンドで使う場合は、ソースコードに直接書かない。.envをリポジトリに含めないだけで安心するのではなく、CI/CDのログ、デバッグログ、例外メッセージに認証ヘッダーが出力されていないかも確認する。認証情報は、通信中だけでなく、ログに残った時点でも漏洩する。

同一オリジン内ではCookieとNonceがWordPressらしい選択肢

WordPressのテーマや管理画面内でJavaScriptからREST APIを呼び出すなら、Cookie認証とX-WP-Nonceヘッダーの組み合わせが標準的だ。

ユーザーがWordPressにログインすると、ブラウザにはログインセッションを示すCookieが保存される。その状態で、サーバー側から発行したNonceをJavaScriptに渡し、REST APIへのリクエストにX-WP-Nonceとして付与する。WordPressは、Cookieによるログイン状態とNonceを確認して、リクエストを送ったユーザーを識別する。

基本的な流れは次のようになる。

1. PHP側でREST API用のNonceを生成する

2. wp_localize_script()などを使って、必要な値をJavaScriptへ渡す

3. JavaScriptからX-WP-Nonceヘッダーを付けてREST APIを呼ぶ

4. エンドポイント側で、ログインユーザーの権限を確認する

管理画面の拡張、カスタムブロック、テーマ内の処理では、この方式が自然だ。WordPressのログインセッションをそのまま利用でき、別のトークン発行基盤を追加する必要もない。

Nonceは、ログインユーザーの権限そのものではない。Nonceを検証しただけで、そのユーザーが投稿を編集できると判断してはいけない。Nonceは主に、正規の画面から送られたリクエストかどうかを確認するためのCSRF対策として使われる。実際に何を許可するかは、current_user_can()などによる権限チェックで決める。

Nonceが通ったことと、操作する権限があることは別の話だ。CSRF対策と認可を一つのチェックで済ませようとすると、後で必ず境界が崩れる。

Cookie認証をクロスオリジンへ無理に持ち出さない

Cookie認証は、同じWordPressサイトの中で使うときに扱いやすい。ところが、フロントエンドを別のオリジンに置くと、Cookieの送信条件、SameSite属性、CORS、認証情報付きリクエストなど、確認すべき項目が一気に増える。

異なるオリジン間でCookieを送る場合、単にURLが別サブドメインであるだけでも、ブラウザのポリシーやサーバー側の設定が関係する。Access-Control-Allow-Origin: *と認証情報付きリクエストを組み合わせることもできない。設定を少し間違えると、開発環境では動くのに本番のブラウザだけで失敗する。

そのため、別オリジンのSPAからWordPressを操作する場合は、Cookieを使い続ける構成が本当に必要かを先に考えたい。同一オリジンにまとめる、サーバー側のBFFを挟む、バックエンドからWordPressを呼ぶなど、認証方式を増やさずに済む設計があるなら、そちらのほうが保守しやすい。

クロスドメインでREST APIを呼ぶときのJWT

フロントエンドをNext.jsやVueで実装し、WordPressをCMSとして別のオリジンに置く構成では、JWT認証が候補になる。リクエストにAuthorization: Bearer <トークン>の形式でトークンを付け、WordPress側で署名や有効期限を検証する方式だ。

JWTの利点は、Cookieに依存せず、リクエストごとにトークンを送れることにある。ブラウザ、モバイルアプリ、別サーバーなど、WordPressと異なる場所にあるクライアントから呼び出しやすい。サーバー側がセッションを保持しなくても、トークンの内容と署名を使ってリクエストを検証できるため、ステートレスな構成とも相性がよい。

ただし、ステートレスであることは、運用が簡単という意味ではない。トークンを発行した後、特定のトークンだけを無効化したくなったとき、サーバー側に状態を持たない設計が逆に足かせになる。

JWTではトークンの寿命と失効方法を決める

JWTを使うなら、最低限、次の項目を決めておく必要がある。

  • アクセストークンの有効期限
  • 期限切れ後の再発行方法
  • リフレッシュトークンを使うかどうか
  • 漏洩したトークンを個別に無効化できるか
  • 署名鍵をどのように保管し、変更するか
  • ブラウザ側のどこにトークンを保存するか

アクセストークンを長期間有効にすると、漏洩時の被害が長引く。短くすると、再認証やリフレッシュの処理が必要になる。リフレッシュトークンを導入すれば、今度はその保管と失効が課題になる。

WordPressコアにはJWT認証の仕組みは標準搭載されていないため、通常は対応プラグインや独自実装を使うことになる。プラグインを選ぶなら、インストール数や検索順位だけで決めるのではなく、更新状況、対応するWordPressやPHPの範囲、トークン失効の扱い、秘密鍵の設定方法を確認したい。

JWT対応プラグインが提供する機能は、製品ごとに差がある。ログイン時にトークンを発行できても、ログアウト時の失効や権限変更時の扱いまで用意されているとは限らない。導入前に「発行できるか」だけでなく、「漏れた後にどう止めるか」を確認するのが実務の順番だ。

トークンをブラウザへ置く場所にも注意する

ブラウザからJWTを使う場合、トークンの保存場所は実装上の細部ではない。保存場所によって、想定する攻撃への強さや、実装の複雑さが変わる。

JavaScriptから読み出せる保存領域に長期間有効なトークンを置けば、クロスサイトスクリプティングの影響を受けたときに読み取られる可能性がある。反対に、Cookieに寄せれば、今度はCSRF対策やCookie属性、CORSの設計が必要になる。

どの保存方法にも代償がある。単に「JWTだから安全」と考えるのではなく、トークンの寿命を短くする、権限を限定する、更新用の情報を分離するなど、漏洩を前提に被害を狭くする設計が必要だ。

OAuth 2.0はJWTとは別のレイヤーにある

OAuth 2.0は、第三者アプリケーションに対して、ユーザーの代わりに一定の権限を委譲するための認可フレームワークだ。ユーザーがWordPressの認証画面で許可し、外部アプリケーションがアクセストークンを受け取り、そのトークンでAPIを呼ぶ。このような流れを標準化することがOAuth 2.0の役割になる。

JWTは、そのアクセストークンを表現する形式として使われることがある。しかし、OAuth 2.0のトークンが必ずJWTであるわけではないし、JWTを使っているからOAuth 2.0になるわけでもない。

この関係を「OAuth 2.0はJWTの上位互換」と表現するのは誤りだ。OAuth 2.0は認可の枠組み、JWTはトークンの形式またはトークンベース認証で使われる技術であり、競合する上下関係ではない。

個人開発でOAuth 2.0を検討するのは、ユーザーが自分のWordPressアカウントを使って、第三者サービスに明示的な権限を与える必要がある場合だ。自分が所有するLaravelとWordPressを接続するだけなら、OAuth 2.0を導入しても得られる利益は限られる。アプリケーションパスワードや、サーバー間の別の安全な認証で足りるケースが多い。

OAuth 2.0では、認可サーバー、リダイレクトURI、アクセストークン、スコープ、トークンの更新などを設計する必要がある。ユーザーごとに権限を分けたい、外部サービスが複数ある、ユーザー自身が接続と解除を行う、といった要件がないなら、最初から採用する方式ではない。

カスタムエンドポイントで必ず確認するpermission_callback

既存の投稿やユーザー情報を扱うだけなら、WordPress標準のREST APIが権限処理を受け持ってくれる。問題は、個人開発でよく作る独自エンドポイントだ。

register_rest_route()でカスタムエンドポイントを追加するとき、認証方式を設定しただけでは十分ではない。誰に、その操作を許可するのかをpermission_callbackで明示する必要がある。

WordPress 5.5以降では、RESTルートの登録時にpermission_callbackを指定することが求められる。省略すると、開発者向けの警告が出る。古いコードでは警告を無視して運用されているケースもあるため、既存案件を引き継いだときは、ルート登録部分を確認したい。

ここで注意したいのは、permission_callbackを省略したからといって、すべてのバージョンで同じように「認証なしで通る」「必ず拒否される」と単純に決めつけられないことだ。WordPressのバージョンや実装、コールバックの内容によって挙動を確認する必要がある。少なくとも、省略を安全策として扱ってはいけない。

公開エンドポイントなら、意図的に公開することを明示するため、__return_trueを設定する。ログインユーザーだけに限定するなら、is_user_logged_in()などを使う。投稿の作成や更新など、特定の権限が必要なら、current_user_can()で必要な能力を確認する。

たとえば、記事を更新するエンドポイントであれば、次のような観点を分けて考える。

  • リクエストを送ったユーザーを認識できているか
  • そのユーザーがログインしているか
  • 対象の記事を編集する権限があるか
  • リクエスト本文の値を検証しているか
  • 更新対象のIDやステータスを勝手に変更できないか

認証ヘッダーが存在することと、操作を許可してよいことは同じではない。アプリケーションパスワードで正しく認証できても、そのユーザーに不要な権限があれば、API経由で過剰な操作ができてしまう。

公開ルートと保護ルートを明示的に分ける

カスタムエンドポイントを作るときは、公開する処理と保護する処理を同じルートに詰め込まないほうがよい。

たとえば、公開ページに表示するためのカテゴリ一覧は認証なしで返せても、管理画面からカテゴリを追加・削除する処理には別の権限が必要になる。記事の一覧取得と、記事のステータス変更も同じだ。

GETだから必ず公開、POSTだから必ず保護、という分け方だけでは足りない。GETでも非公開記事やユーザー情報を返すなら認証が必要になるし、POSTでも問い合わせフォームのように公開利用を前提とする処理は存在する。HTTPメソッドではなく、返すデータと実行する操作の性質で判断する。

permission_callbackだけでも、認証だけでも足りない

よくある誤解は、「認証が通ったので安全」「Nonceを検証したので安全」「permission_callbackを設定したので安全」という、一つのチェックに依存する設計だ。

実際には、認証、認可、入力値の検証、出力データの制御を別々に考える必要がある。

認証は、誰がリクエストを送ったかを確認するものだ。認可は、そのユーザーに操作を許可するかを判断するもの。入力値の検証は、受け取ったデータを安全な形式や範囲に限定するものだ。

独自エンドポイントでは、permission_callbackの中で権限を確認し、処理本体でも入力値を検証する。IDを受け取るなら、存在する投稿か、対象ユーザーがその投稿を編集できるかまで確認する。公開するデータも、管理画面で見える情報をそのまま返すのではなく、APIに必要な項目だけに絞る。

個人開発での認証方式の選び方

認証方式を選ぶとき、最初に見るべきは「最新かどうか」ではない。次の4点を並べると、かなり判断しやすくなる。

  • APIを呼ぶのはサーバーかブラウザか
  • WordPressと呼び出し元は同一オリジンか
  • 操作するユーザーは自分だけか、複数ユーザーか
  • トークンや資格情報を失効させる仕組みを運用できるか

この4点を決めると、選択肢は自然に絞られる。公開記事の表示だけなら認証なしのGETで足りる可能性がある。バックエンドから非公開データを取得したり、投稿を書き換えたりするなら、サーバー間の認証が必要になる。ブラウザ上の複数ユーザーが個別の権限で操作するなら、セッションやトークンの扱いを別に設計しなければならない。

Laravelやバッチから呼ぶならアプリケーションパスワード

自分のLaravelアプリケーションからWordPressを操作するなら、まずアプリケーションパスワードを検討する。ブラウザに資格情報を渡さず、Laravel側の環境変数に保存してサーバー間通信に限定できるからだ。

取得だけなら読み取り用の権限に近いユーザーを使い、投稿更新が必要ならその用途に合わせたユーザーを用意する。連携先が増えたときは、アプリケーションパスワードも分ける。1つのキーをあらゆる処理で使い回さない。

アプリケーションパスワードには、JWTのようなアクセストークンの更新処理がない。その代わり、定期的な棚卸しと、不要になった資格情報の取り消しが必要になる。更新処理が少ないことは利点だが、何もしなくてよいという意味ではない。

Laravel側では、認証情報を使う処理の境界も明確にしておきたい。ブラウザからLaravelへ送られた値を、そのままWordPress APIのURLや認証ヘッダーへつなぐ設計は避ける。WordPressへの接続はLaravelのサーバー側に閉じ込め、受け取ったパラメータは許可した操作と値の範囲に限定する。

WordPress内のJavaScriptならCookieとNonce

テーマやプラグイン、管理画面のカスタムブロックなど、WordPressのログインセッションを利用できる場所なら、CookieとNonceが自然だ。

この構成でJWTを持ち込むと、トークンの発行、保存、更新、失効を別に設計することになる。すでにWordPressが持っているログインセッションを使えるのに、もう一つ認証基盤を作る理由は薄い。

もちろん、CookieとNonceを採用しても、権限確認とCSRF対策が不要になるわけではない。Nonceの検証、current_user_can()による認可、入力値の検証をそれぞれ実装する。

WordPress内の画面であっても、管理者向けの処理を一般ユーザーへ公開してよいとは限らない。ログインしているかどうかではなく、対象の操作に必要な能力を確認する。投稿の編集、設定の変更、ユーザー情報の取得などは、個別に権限の境界を考える必要がある。

別オリジンのSPAやモバイルアプリならJWTを慎重に検討する

Next.jsやNuxtなどのSPA、あるいはモバイルアプリからWordPressを操作するなら、JWTの利便性は高い。Cookieの制約を避けやすく、リクエストに明示的なBearerトークンを付けられるからだ。

ただし、JWTを選んだ時点でプラグインや独自実装の保守が始まる。CORSの許可オリジン、トークンの保存場所、失効方法、署名鍵の管理、更新時の互換性を決めなければならない。

単に記事を公開表示するだけなら、そもそも認証付きAPIが必要ない可能性もある。公開記事の取得にログイン認証を付け、CORSやトークン管理を増やすのは、要件に対して過剰な場合がある。書き込みや管理者向け操作だけを認証対象にするほうが、構成は小さくできる。

OAuth 2.0は要件が明確な場合だけ

第三者のサービスにユーザー自身が権限を与える必要がある、複数の外部アプリケーションをユーザー単位で管理したい、許可する操作をスコープで分けたい。このような要件があるなら、OAuth 2.0を検討する価値がある。

逆に、WordPressと自分のバックエンドをつなぐだけなら、OAuth 2.0を採用する前に、アプリケーションパスワードやサーバー間通信で要件を満たせないか確認したい。認可の仕組みを必要としない連携に、認可フレームワークを持ち込む必要はない。

認証方式より先に決めるべき運用設計

認証方式の比較では、実装方法に目が行きやすい。しかし、事故が起きるのはコードを書いた瞬間より、その後の運用である。

アプリケーションパスワードを使う場合は、連携先ごとに発行し、名前を付けて管理する。使わなくなったら取り消す。サーバーのログやエラーログに認証情報が出ていないか確認する。秘密情報はコードに直書きせず、環境変数やホスティングサービスのシークレット管理を使う。

JWTを使う場合は、プラグインの更新状況を追いかける。WordPress本体やPHPを更新する前に、認証プラグインが対応しているか確認する。アクセストークンを長期間有効にしない。漏洩時に、すべてのユーザーを巻き込まずに対象のトークンやセッションを止められるか確認する。

CORSを設定する場合は、許可するオリジンを具体的に指定する。*で通してから認証を足す構成は、動作確認としては楽でも、本番設定としては危険だ。許可するメソッドとヘッダーも必要な範囲に限定する。

独自エンドポイントでは、認証方式を決めた後にpermission_callbackを書くのではなく、ルートを設計する段階で権限を決める。公開読み取りなのか、ログインユーザー向けなのか、編集者以上なのか。権限の境界が曖昧なまま実装を始めると、後からエンドポイント単位の例外が増えていく。

認証方式を選ぶ作業は、トークンの種類を選ぶ作業ではない。通信する主体、権限の範囲、漏洩時に止める手段まで含めて決める作業だ。

資格情報を分けるだけで、復旧の手順が変わる

個人開発では、認証情報の管理を自分一人で行うことが多い。そのため、複雑なアクセス管理の仕組みを導入するより、まずは連携単位を分けるほうが効果を出しやすい。

Laravel用とバッチ用でアプリケーションパスワードを分けておけば、一方のサーバーを停止したときに、もう一方の処理まで止める必要がなくなる。検証環境と本番環境を分ければ、開発中の設定ミスが本番の資格情報に直結することも避けやすい。

名前を付けて管理するのも地味だが効く。発行した時点では用途を覚えていても、数か月後にはどの資格情報がどの処理に使われているか分からなくなる。資格情報の名前、使用箇所、不要になったときの取り消し方法を記録しておくと、障害対応のときに迷わない。

「運用でカバーする部分」と「コードで解決する部分」

個人開発では、すべてを高機能な認証基盤で解決しようとしないほうがよい。コードで守る部分と、運用で確認する部分を分けると、構成が見通しやすくなる。

コード側で最低限実装するのは、次のような項目だ。

  • 独自ルートへのpermission_callback設定
  • current_user_can()などによる権限確認
  • Cookieを使う場合のNonce検証
  • リクエスト本文、ID、クエリパラメータの入力値検証
  • CORSの許可オリジン制限
  • 認証失敗時に秘密情報を返さないエラーレスポンス
  • APIで返すデータの範囲を必要最小限にすること
  • 公開データと非公開データを同じ条件で返さないこと

運用側では、次の項目を定期的に確認する。

  • 発行済みアプリケーションパスワードの棚卸し
  • 退役した連携の資格情報の取り消し
  • JWT対応プラグインの更新状況
  • WordPress本体とPHPのアップデート計画
  • 本番環境でHTTPSが維持されているか
  • .envやCI/CDのログに秘密情報が残っていないか
  • 管理者権限を外部連携に使っていないか
  • CORSの許可先に不要なオリジンが残っていないか

「一定期間ごとに必ずローテーションする」といった運用ルールを決めるのもよいが、実行できないルールは存在しないのと同じだ。個人開発なら、連携を追加したとき、担当を変えたとき、環境を廃止したときに必ず資格情報を見直す、といった現実的なタイミングから始めるほうが続く。

最初の設計では、「どの認証方式で、どのオリジンから、どのエンドポイントを、どの権限で呼ぶのか」を書き出しておきたい。1ページの簡単なメモでよい。ここを決めずに実装を始めると、あとからブラウザ対応のためにJWTを追加し、さらにCORSを緩め、最後に権限チェックを足すという順番になりやすい。

その状態になると、どの設定が安全性を担保しているのか分からなくなる。認証方式を後から付け替えるのが難しいのは、コードの変更量だけが理由ではない。Cookie、CORS、権限、ユーザー、外部サービスの構成が一緒に絡むからだ。

個人開発で選ぶべき現実的な線引き

WordPress REST APIの認証方式を比較するとき、JWTやOAuth 2.0が高度で、アプリケーションパスワードが簡易に見えるかもしれない。しかし、個人開発の現場では、高度な仕組みを選ぶことが安全性に直結するわけではない。

LaravelやCLIからサーバー間で呼ぶなら、HTTPSを前提にアプリケーションパスワードを使う。WordPress内のJavaScriptから呼ぶなら、CookieとNonceを使う。別オリジンのSPAやモバイルアプリで、Cookieを使わない構成が必要なら、JWTを導入する。ただし、トークンの有効期限と失効方法まで決められる場合に限る。第三者への権限委譲が必要なら、OAuth 2.0を認可の仕組みとして検討する。

公開済みの記事を表示するだけなら、認証なしのGETで十分なことが多い。非公開データの取得や投稿の更新、メディアの登録など、権限が必要な操作だけを認証対象にする。これだけでも、ブラウザへ渡す秘密情報や、不要なCORS設定を減らせる。

この順番で考えれば、認証方式の名前に引きずられにくい。

WordPress 5.6以降のアプリケーションパスワードは、個人開発者にとって十分に実用的な標準機能だ。Basic形式であることを理由に避ける必要はない。重要なのは、HTTPSで通信を保護し、連携先ごとに資格情報を分け、必要な権限だけを与えることだ。

一方で、ローカル環境のHTTPが動いたからといって、本番でも同じ構成にしてよいわけではない。JWTを使えるからといって、すぐに採用する必要もない。OAuth 2.0が本格的だからといって、JWTより上位の方式というわけでもない。

最新の方式を導入することより、半年後に自分で更新でき、漏洩時に止められ、権限の範囲を説明できることのほうが大切だ。コア機能で済む連携はコア機能で済ませる。独自エンドポイントではpermission_callbackを省略しない。ブラウザとサーバーの境界を曖昧にしない。

個人開発という制約のなかでWordPress REST APIを安全に使うなら、必要なものだけを選び、増やした仕組みを最後まで運用する。その地味な判断こそが、深夜に401や謎のCORSエラー、そして認証プラグインの更新停止に追い回されないための、いちばん確実な対策になる。

Related reading: ヘッドレスWordPressのAPI設計:RESTとGraphQLの性能差と選択基準 and Laravel SanctumによるSPA認証:クッキーとトークンのどちらを選ぶべきか.

よくある質問

WordPress REST APIでアプリケーションパスワードを使うメリットは何ですか?
WordPressコアの標準機能であるため追加プラグインが不要で、用途ごとに資格情報を分けて管理できるため、漏洩時の影響範囲を限定できる点です。
LaravelからWordPressのAPIを呼び出す場合、どの認証方式が適していますか?
アプリケーションパスワードが適しています。サーバー間通信として利用し、HTTPSで通信経路を保護した上で、環境変数などで資格情報を安全に管理してください。
JWT認証を導入する際に注意すべき点は何ですか?
トークンの有効期限や失効処理、署名鍵の管理方法を設計する必要があります。また、プラグインの更新状況や、トークンの保存場所によるセキュリティリスクも考慮しなければなりません。
WordPressの管理画面内でJavaScriptからAPIを呼ぶにはどうすればいいですか?
CookieとNonceの組み合わせが標準的です。ログインセッションを利用できるため、別の認証基盤を追加することなく、CSRF対策としてNonceを付与してリクエストを行います。
OAuth 2.0はJWTの上位互換ですか?
いいえ、異なります。OAuth 2.0は第三者への権限委譲を行うための認可フレームワークであり、JWTはトークンの形式や認証技術を指すため、役割が異なる別レイヤーの仕組みです。
カスタムエンドポイントで認証を設定したのに安全ではないと言われるのはなぜですか?
認証ヘッダーの確認だけでは不十分だからです。permission_callbackで誰に操作を許可するかを明示し、入力値の検証やcurrent_user_canによる権限チェックを個別に実装する必要があります。

参考情報