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

# 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 は、```GET``` と ```POST``` のみ

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

## 熟読すべき資料

- [オリジン間リソース共有 (CORS) - MDS Web Docs](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
- [なんとなく CORS がわかる...はもう終わりにする。](https://qiita.com/att55/items/2154a8aad8bf1409db2b)

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

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

### 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

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

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

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


### Access-Control-Allow-Headers

今回の場合、```Authorization``` と ```Contet-Type``` と ```Origin```を追加する必要がある。

```javascript
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 とか使う場合は、許可する必要がある。必要なものをカンマ区切りで列挙する。例えば、

```javascript
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 の方が正しいですね。

```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245908/26381d66-f5c4-e134-d44c-25eeba8c28cf.png)
![Screen Shot 2021-12-18 at 7.19.11 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245908/f0f36a80-48d7-8650-a2f3-b3d355d551fb.png)
完全勝利である。Happy Holidays!