Basic認証が必要な POST API の CORS でハマった話

スポンサーリンク

はじめに

先日書いた下記のメモ。

React から FuelPHP APIをコールする際の CORS 回りのメモ

これですべて CORS 関連は解決したと思っていたのですが、全然甘かった(/_\*)
表題の通りですが、ほんと CORS って深いですな〜
というか、今回は完全に暗礁に乗り上げてしまったので、その辺を備忘録として残しておきたいと思います。
というか、自分の理解が間違っているだけかもしれないので、ご存じの方がいればご教授願いたい(/_\*)

最初に書いておくと、自分の結論としては下記のページで書かれているとおりです。
Basic 認証が必要な領域への Preflight リクエストは対応できないので、認証を外すか、別の手段で認証するか、しかないと結論を出しました。

Basic認証の領域へプリフライトでリクエストしてハマった - Qiita
概要 CORS(クロス オリジン リソース シェアリング)となるリクエストを出したのにうまくレスポンスが戻らない。こんな時、プリフライトという仕組みに悩まされたエンジニアも多いでしょう。私もそんなエンジニアの1人でしたが、もう克...

やりたいこと

JavaScript 側から API サーバへ POST API を実行する。
その際、API サーバ側は Basic 認証を必要とする。
また、この API は request/response を JSON でやりとりするため、HTTPヘッダに Content-Type: application/json の付与が必須である。

上記のような API コールは例えば下記のようなコードとなります。

  fetch(url, {
    method: "post",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
    mode: "cors",
    credentials: "include",
  })
    .then((res) => res.json())
    .then(console.log)
    .catch(console.error);

前回と違う部分

下記の2つです。

  • 前回は GET だが、今回は POST
  • Content-Type: application/json をヘッダに付与している

直接的な問題は後者により、Preflight リクエストが発生するようになってしまったことにより挙動が変わってしまいました。
ただ、Content-Type: application/json の指定は POST となったことに起因しているので、大本で言えば POST API に対して新たな問題が出たことになります。

Preflight リクエストの問題

Preflight リクエストは各所で解説されているので省略しますが、これにより POST リクエストが送信される前に OPTIONS リクエストが API サーバ側に送信されます。
今回の場合は Basic 認証領域への OPTIONS リクエストになるので、Preflight リクエストにも認証情報を載せる必要があります。
ブラウザ上では Basic 認証は通した状態で試しているので、OPTIONS リクエストにも Basic 認証の情報が載っていると思ったのですが、どうやら Preflight リクエストの際にはブラウザ側で特定のヘッダしか載せないような仕様となっているようで、どうやっても Preflight リクエストで Basic 認証を通すことができず 401 となってしまうことが判明しました。

ちなみに JavaScript のコンソール上のエラーとしては下記のように出ます。

Access to fetch at 'https://xxxx/admin/api/yyyy' from origin 'http://localhost:8888' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

やったこと

Preflight リクエストである OPTIONS リクエストでは Basic 認証を通過することができないと判断したため、OPTIONS リクエストは Basic 認証を適用しないようにすることを考えました。

具体的には下記のような .htaccess を Basic 認証領域のトップに置きました。

Header set Access-Control-Allow-Origin "http://localhost:8888"
Header set Access-Control-Allow-Methods "OPTIONS, GET, POST, PUT, DELETE"
Header set Access-Control-Allow-Headers "*, Authorization, Content-Type"
Header set Access-Control-Allow-Credentials "true"

<LimitExcept OPTIONS>
  AuthType Basic
  AuthUserFile /xxx/htpasswd
  AuthName "xxx admin"
  Require valid-user
</LimitExcept>

Header set については、前回書いた React から FuelPHP APIをコールする際の CORS 回りのメモ とほぼ同様ですが、今回は Content-Type: application/json を使っているので、Access-Control-Allow-HeadersContent-Type も追加しています。

そして、問題の Basic 認証ですが、LimitExcept 指定で OPTIONS リクエスト以外の時に Basic 認証するように設定しました。
これにより OPTIONS リクエストの時には Basic 認証がかからなくなります。

OPTIONS リクエストへの対応

これでOKだろうと思ったのですが、なぜか下記のようなエラーが console に出ました。

Access to fetch at 'https://xxxx/admin/api/yyyy' from origin 'http://localhost:8888' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

よくよくメッセージを読んでみると、”It does not have HTTP ok status.” だそうで。
OPTIONS リクエストできちんと 200 ok が返っていなかったのが原因でした(/_\*)
たぶん、OPTIONS リクエストのための実装などはしていないことがほとんどだと思うので、明確に該当 OPTIONS リクエスト用に実装しておくか、.htaccess の RewriteRule あたりで 200 ok を返すように設定しておきましょう。

さいごに

以上で一応想定通りの動きが実現できましたが、CORS と Basic 認証の組み合わせはやばすぎますな。
設計として、ブラウザの JavaScript から呼び出されるであろう API サーバに Basic 認証をかけるのは極力やめた方が良いですな。
もしくは、Basic 認証をかけた API サーバにアクセスするのであれば、ブラウザの JavaScript から直接ではなくて BFF 的な中間レイヤーを経由してアクセスするようにすることも検討しても良いかなと思いました。

コメント

タイトルとURLをコピーしました