43
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Bearer認証付きAPIでCORSエラーとたたかった記録

Last updated at Posted at 2021-12-18

CORS 強かった…

Next.js + typescript + prisma + PostgreSQL で Bearer 認証付き API を作って、サーバに Deploy したら、CORS エラーでアプリ側から全然呼べなかった。アプリ側は、React.js で axios を使っていて、build したものが Deploy されている。

"Access-Control-Allow-Origin": "*"を header に付けてやれば良いだけでしょと舐めてたが、そんな簡単には解決しなかった。にわか知識では突破できない CORS。強かった。

Preflight Request の理解不足が今回のハマりポイントだった。

API の概要

  • header の "Authorization": "Bearer {access_token}" で認証
  • Request の Content-Type は、application/json
  • 認証が必要な API は、Bearer 認証をする middleware を通過する
  • API が提供する Method は、GETPOST のみ

とても一般的な構成じゃないかと思う。開発中は、Local で POSTMAN から呼び出して確認していた。ブラウザでは、POSTMAN や curl で確認した通りに動くわけではないので、注意が必要だ。という教訓。

熟読すべき資料

これが完全に分かれば、ハマりポイントはほぼない。けど、難しい。

ググると出てくるけど使えなかった解決策

Proxy 使う

でも、React で Build すると使えないから注意。というもの。確かに Proxy 使うと CORS エラーは解決するんだけど、Build するから使えなかった。

Preflight リクエストにならないようにする

GET と POST しか使ってないから Simple Request じゃないの?と思ってましたが、今回は Authorization ヘッダーや、application/json な Content-Type を使うので、Preflight Request が走ってしまうので、使えなかった。

header で Bearer 認証しなければ、これらで解決できる。うっかり、訳も分からず、Local での開発で、Proxy 設定しちゃってたりすると、発見が遅れる。

今回のポイント

Authorization ヘッダーが付いているので、GET だろうが POST だろうが、Preflight Request を回避する方法はない(fetchAPI で mode: no-cors とかはなしで)ため、Preflight Request をどう捌くかがポイントになる。

Header をセットする

以下、コードは Next.js だが、まぁどれでも似たようなものでしょう。きっと。

Access-Control-Allow-Origin

res.setHeader("Access-Control-Allow-Origin", "*");

なお、* はよくないのだが、どこから呼ばれるか分からん API だし、token を発行する際の認証、token による認証を行うので、ノーガードではないからいったん良しとした。

ただし、Cookie や認証ヘッダー、TLSクライアント証明書などを送る場合、Access-Control-Allow-Credentialstrue としてセットする必要があり、それをセットすると、ワイルドカードは使えなくなり、個別に設定する必要が生じる。ここで言う認証ヘッダーは、今回の Authorization ヘッダーとは別物で、例えば、Cookie を送信することでセッションを共有したいみたいな場合に、送信元の axios 等で withCredentials = true などとすると、対応が必要になる。今回は必要ないので、ワイルドカードのまま。

Access-Control-Allow-Headers

今回の場合、AuthorizationContet-TypeOriginを追加する必要がある。

res.setHeader(
  "Access-Control-Allow-Headers",
  "Origin, X-Requested-With, Content-Type, Authorization, Accept"
);

Content-Type は、CORS-safelisted request header(Accept, Accept-Language, Content-Language, Content-Type)なので、明示的に宣言する必要は、通常は、ないのだが、暗黙の Content-Type には制約があり、

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

この3種類しか受け付けない。今回は、application/json を送りたいので、明示的に宣言する必要がある。

Origin, X-Requested-With は、Preflight Request で送られてくるらしいので、付ける必要がある。

Accept も同様に制約があるので、心当たりはないが、あちこち見たところ付いているので、念の為付けておいた。

と、なんだか曖昧になっているが、実際に Preflight Request の Request Header を見てみると、

  • Origin 送られてきている
  • Accept 送られてきている
  • X-Requested-With 送られてきていない

ので、X-Requested-With は、必要なければ、付けなくても良いかも知れない。実際の動作的には、Authorization, Content-Type だけ追加しておけば動く。

他にも、x-oath-token とか、そんなようなカスタム header を送る場合は、カンマ区切りで使うものを列挙する必要がある。

Access-Control-Allow-Methods

GET, POST, HEAD, OPTIONS だけの場合は不要。なので、今回は不要。DELETE とか PUT とか使う場合は、許可する必要がある。必要なものをカンマ区切りで列挙する。例えば、

res.setHeader(
  "Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE, PUT"
);

こんな感じで。
CORS 対策とは関係ないが、クリックジャッキング対策で、X-FRAME-OPTIONS: DENY などを付けておくと安心。

Preflight Request の OK Status を返す

Header をセットして、これで完璧!と思いきや、

Access to XMLHttpRequest at 'http://localhost:3000/api/xxx' from origin 'http://localhost:3001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

こんなエラーが出てしまう。ショックのあまり、エラーメッセージをきちんと読めず、再度 CORS 対策をググって、合ってると思うんだけどなーと時間を無駄にすることになる。私は超無駄にした。

エラーメッセージには、「Preflight request が Access Control チェックを通らなかった: HTTP ok ステータスがない。」と書いてある。

Preflight Request に対するレスポンスを設定していないのが原因。

確認してみると、Preflight Request に対するレスポンスは、今回の私の作った API の場合、401 Unauthorized が返っていた。Preflight Request は、リクエスト送って良いですか確認のリクエストなので、送信元でセットしたつもりのカスタムヘッダーの Authorization は送られない。送られてこないので、認証エラーとなり、API 側でセットした 401 ステータスが返る。

という訳で、Preflight Request には、OK と返してあげないといけない。Preflight Request の Method は、OPTIONS なので、その場合は、200 を返してあげる。OK だけど、Access-Control-Allow-xxx で設定したのに反した場合は、ブラウザは本リクエストを送ってこないので、OK を返して良い。

(追記): 当初 200 返したけど、中身がないので、204 の方が正しいですね。

if (req.method === "OPTIONS") {
  return res.status(204).end();
}

Preflight Request 内で、何か確認する必要がある場合は、ここで処理を書けば良いが、そこまで分かってる人は、この記事に来たりはしないだろう。

CORS、完全に理解した

Chrome で Inspect して、Network タブで確認する。

Screen Shot 2021-12-18 at 7.29.41 PM.png
Screen Shot 2021-12-18 at 7.19.11 PM.png
完全勝利である。Happy Holidays!

43
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?