AWSのALBでOIDC認証を設定したらmanifest.jsonがCORSで弾かれるようになった件

開発中のWebサイトにApplication Load BalancerでOIDC認証を設定したらmanifest.jsonの読み込みが失敗するようになりました。
原因調査と対応に半日くらいかかったので調査結果をまとめておきます。

# 事象
冒頭に書いたとおり、開発中のWebサイトにApplication Load Balancerでgoogle認証を設定したところ、下記のエラーが発生するようになりました（エラーはChrome DevToolsのConsoleで確認）

```console
Access to manifest at 'https://accounts.google.com/o/oauth2/v2/auth?client_id=...'
(redirected from 'https://example.com/manifest.json') 
from origin 'https://example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
※ URLは書き換えています

Chrome DevToolsのNetworkを確認すると下記のようになっていました。
Webページは認証後に正しく表示されて問題なく動作しているのですが、manifest.jsonへのリクエストが302となっており、リダイレクト先のauth（ログイン画面）はfailedになっています。
![スクリーンショット 2020-09-01 23.50.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/945e8f50-87bb-39cc-18f7-b53e291b32fc.png)

Chrome DevToolsのApplication > Manifestで確認してみましたが、manifest.jsonは読み込まれておらず "No manifest detected" となっていました。
![スクリーンショット 2020-09-01 23.35.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/ba848348-27ad-612c-b2c0-32918fc5737f.png)

# 補足

調査結果を載せる前にいくつか補足しておきます。

## Application Load Balancerの認証とは

Application Load Balancerにアクセスした時にOpenID Connect(OIDC)の認証を行うように設定することができます。
これを使うと、アプリケーションに認証を実装しなくて良くなるのでかなり便利です。
詳細はドキュメントをご覧ください。
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/listener-authenticate-users.html

## CORSとは
Cross-Origin Resource Sharingの略です。
CORSは別ドメインへのリソースアクセスを制御し、Cross-Site Request Forgery(CSRF)などのセキュリティー攻撃を防ぐための仕組みです。

詳細はドキュメントをご覧ください。
CORS
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
CSRF
https://developer.mozilla.org/ja/docs/Glossary/CSRF

## Manifestとは

MDNから引用
https://developer.mozilla.org/ja/docs/Web/Manifest

> ウェブアプリマニフェストは、プログレッシブウェブアプリ (PWA) と呼ばれる一連のウェブ技術の一部であり、アプリストアを通さずに端末のホーム画面にインストールすることができるものです。単純なホームスクリーンリンクやブックマークを持つ通常のウェブアプリとは異なり、 PWA は事前にダウンロードしてオフラインでも動作するだけでなく、通常の Web API を使用することもできます。

今回開発しているWebサイトではぶっちゃけなくてもよいのでエラーを解決できなかったらmanifest.jsonへの参照を消そうと思ってましたｗ

# 調査結果

まずはmanifest.jsonがどこで読み込んでいるか確認しました。
ちなみにWebサイトはReactで作成しています。

public/index.htmlの中で`<link>`で読み込んでいました。

```html:public/index.html
<head>
  ...
  <link rel="icon" href="/favicon.ico" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
  ...
  <link rel="manifest" href="/manifest.json" />
  ...
  <script charset="utf-8" src="/static/js/3.9de5dff2.chunk.js"></script>
</head>
```

今回の事象でまず気になるのはmanifest.jsonだけリダイレクトされている点です。
manifest.json以外の`<link>`で読み込んでいるfavicon.icoやcss、`<script>`で読み込んでいるjsは問題なく読み込めています。

## icoやcss、jsファイルには認証が効いていない？
最初に疑ったのは、icoやcss、jsには認証が効いておらず、manifest.jsonは認証がかかっているのでは？ということです。
他のファイルは認証が効いてないから読み込めており、認証が正しく効いているmanifest.jsonはリダイレクトされてしまっているのでは？ということです。
確かめるために認証されていない状態のブラウザで`https://example.com/favicon.ico`のようにURLを直接指定して開いてみました。

確認した結果、どのファイルを開くときもログイン画面にリダイレクトされ、認証が成功すると正しく読み込むことができました。
ということは

* 他のファイルも認証がかかっており、認証がOKだから正しく読み込めている。
* manifest.jsonだけは認証NGとなりログイン画面にリダイレクトされている。

ということのようです。

## なぜmanifest.jsonだけCORSで弾かれているのか？

改めてエラー内容を確認します。

```console
Access to manifest at 'https://accounts.google.com/o/oauth2/v2/auth?client_id=...'
(redirected from 'https://example.com/manifest.json') 
from origin 'https://example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
※ URLは書き換えています

重要なのはここです
`No 'Access-Control-Allow-Origin' header is present on the requested resource.`
必要なheaderがないと言っています。

他のファイルも認証は行っているのになぜmanifest.jsonだけ？という先ほどと同様の疑問が湧いてきます。

headerに問題がありそうなので、Chrome DevToolsのNetworkでfavicon.icoとmanifest.jsonのheaderの差分を確認してみました。
下記はそれぞれのheaderの抜粋です。
sec-fetch-xxxに原因となっていそうな差分を見つけました。

```yaml:favicon.ico
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: same-origin
```

```yaml:manifest.json
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin
```

sec-fetch-xxxが怪しいというのはパット見でわかったのですが、このヘッダーについて調べるところで結構苦戦しました・・・

まずMDNはまだDraftで内容がありませんでした・・・
![スクリーンショット 2020-09-02 22.29.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/064c0e6d-d4fe-5851-4781-c230a5fc2f79.png)
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode

検索して見つけた下記のページは日本語が意味不明で理解できず・・・
https://triple-underscore.github.io/webappsec-fetch-metadata-ja.html

諦めず検索し続けて下記にたどり着きました。
https://developer.mozilla.org/ja/docs/Web/API/Request/mode
こちらには下記のように記載されていました。

> no-cors — HEAD か GET、POST 以外のメソッドを防ぎます。

favicon.icoやcss、jsはno-corsがセットされており、GETリクエストなので成功しているようです。

> cors — クロスオリジンリクエストを許可します。たとえば、サードパーティベンダーが提供する様々な API にアクセスできます。これらは、CORS プロトコルに則ることが期待されています。制限されたヘッダーだけが Response で使用できますが、body は読み取り可能です。

manifest.jsonはcorsがセットされているため、CORSのポリシーを守る必要があるようです。

> リクエストがRequest.Requestコンストラクタ以外で生成された場合はmodeとして通常no-corsがセットされます。たとえばマークアップから生成された埋め込みリソースのようなリクエストは、crossoriginアトリビュートが設定されていない限り、no-corsを利用します。そのようなものの例として、`<link>` や `<script>` エレメント（ただしモジュールを除く）、`<img>、<audio>、<video>、<object>、<embed>、<iframe>` エレメントなどが存在します。

これを読むと`<link>`は`sec-fetch-mode: no-cors`がセットされるように思えますが、なぜmanifest.jsonはcorsなのだ・・・

## CORSポリシーを守るには
これまでの調査結果から、manifest.jsonだけ`sec-fetch-mode: cors`がセットされている理由はわかりませんが、CORSのポリシーを守るようにすれば良さそうです。
では守るにはどうすればよいのでしょうか？

それはこちらのページに書いてありました。
https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/crossorigin

> crossorigin 属性は、 `<audio>, <img>, <link>, <script>, <video>` の各要素で有効であり、 CORS への対応を提供し、したがって要素が読み取るデータのために CORS リクエストの構成を有効にします。

crossoriginに指定できるのは下記の2つ。

* anonymous
    * この要素のための CORS リクエストで資格情報フラグを 'same-origin' に設定する。
* use-credentials
    * この要素のための CORS リクエストで資格情報フラグを 'include' に設定する。

use-credentialsを指定すれば良さそうですね。

なお、このページに今回の答えのようなことも書いてありました・・・
最初からこのページを見つけてさえいれば速攻解決したのに・・・

> 資格情報を必要とするマニフェストを読み取るときは、同じオリジンからのファイル読み取りであっても use-credentials の値を使用する必要があります。
`<link rel="manifest" href="/app.webmanifest" crossorigin="use-credentials">`

manifest.jsonの読み込みを下記のように直すことで無事エラーを解決することができました！！

```html
 <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
```

Chrome DevToolsのNetwork
![スクリーンショット 2020-09-02 22.58.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/c9840fe0-f5c1-68a1-985f-fd4b3c1c1a34.png)