samで構築したBFFのCORSエラーが解消できなくて困った話

## はじめに
こんにちは

みなさん、AWS sam使っているでしょうか。
API gateway + Lambdaを利用したアプリケーションの構築や更新頻度の多いアプリケーションではよく利用されるsamですが、今回iOS系でCORSエラーが解消できなくて3、4ヶ月ほど困っていたので備忘録がてら書き残しておこうと思います。
※弊社コンプライアンス的なところが怖いので必要ない部分は特に記載してないです。

## TL;DR

- Api gatewayのCORSの設定は明示的に記載する

### 構成図
プラットフォームはiOS/Androidで以下のような構成図をとっています。
API GatewayとLambdaはAWS samで構築されており、CI/CDパイプラインを通してsam templateからデプロイを行います。

![Untitled Diagram-Page-3 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382072/0fae9790-6b3a-719e-d596-304f2e056ed8.png)

template.ymlはこんな感じでした。
開発メインのメンバーが初期に記載した部分で、これまでこの部分の書き換え等はほぼ行われていませんでした。
CORSの設定が全てワイルドカードになっていますね。

``` yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Googlesensei

# Global Settings
  Api:
    Cors:
      AllowOrigin: "'*'"
      AllowHeaders: "'*'"
      AllowMethods: "'*'"

  Resources: // このあとも続いてるよ
```

### 事象
iOS12系以下の端末でAPにリクエストを送ると **CORSエラーとなり、403で弾かれていました。**
iOS13系、AndroidではCORSエラーが発生することがなく、エラーなくアプリを利用することができていました。

iOS13系はいけるのに、12系以下はエラーを返す理由は明確に調査は行っていませんが。おそらくバージョン間でSafariのCORS設定に差分があり、たまたま13系は突破できたのではないかと推測しています。

### 調査
samと手動で構築した際にAPI gatewayに差分がありました。
手動で構築したAPI gatewayはマネジメントコンソールが裏側でAPIのレスポンスのモデルを作成し、自動的にモデルをセットしてくれていましたが、samで構築したAPIにはモデルが存在せずメソッドレスポンスが指定されていない状態になっていました。

<img width="802" alt="スクリーンショット 2020-07-25 20.31.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382072/c800ce85-b004-2ccb-631e-77dc3210f7be.png">

そしてこれに対しマネジメントコンソールから手動で構築した場合と同じ設定にしてやることで、iOS12系がCORSエラーを取得することなくアプリを利用することができるようになりました。

しかし、 API gatewayのメソッドレスポンスの設定はあくまでも **API gateway側でレスポンスの整形を行う** ことであり、今回のケースのようにLambda Proxyを利用している場合はLambda内で正しいレスポンスの形式で返却することができているなら不要なのです。

これで以下の2点が整理できました。
- 正しく設定が行われていればiOS12系でもアプリ利用が可能
- API gatewayの設定かLambdaのレスポンスに問題が発生している。

#### Lambdaのレスポンス
Lambda Proxy統合を利用している場合、Lambdaのレスポンスは以下の形式にする必要があります。

``` Typescript
{
    "isBase64Encoded": true|false,
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... },
    "body": "..."
}
```

Lambdaの資源を見直すとこの形式になっていたのでLambdaに問題がないことがわかりました。

#### API gatewayの設定
以下になっていました。見た感じ特別な設定は見当たりませんね。
modelsの設定なども試しましたが、AWS samではメソッドレスポンスを設定するプロパティが無く、Cloudformation記法で書くためにはtemplateの大幅改造が必要だったので ~~やりたくない~~ 、別の視点から調査をすることにしました。

``` yml
  Api:
    Type: AWS::Serverless::Api
    TracingEnabled: X-Rayの利用
    Properties:
      Name: Api gatewayの名前
      StageName: Api gatewayのステージ名
      Auth: // 認可の設定
      AccessLogSetting: // アクセスログの設定
```

### CORS設定の見直し
iOS12系以下で表示されているエラーの内容を改めて確認してみました。

```
Request header field Authorization is not allowed by Access-Control-Allow-Headers.
```

AuthorizationがHeaderで渡ってないみたいですね。
Access-Control-Allow-Headersで調べてみると、こんな記載がみつかりました。※markdownの仕様上消えちゃうので大文字で。

>＊ (ワイルドカード)
 "＊" の値は、資格情報のないリクエスト (HTTP Cookie や HTTP の認証情報のないリクエスト) の特殊なワイルドカード値です。認証情報付きのリクエストでは、特別な意味のない "＊" というヘッダー名として扱われます。なお、 Authorization ヘッダーはワイルドカードで表すことができず、常に明示的に列挙する必要があります。


IAMを使っているのでワイルドカードをやめて、Authorizationも含めて明示的に記述をしてみます。
細かく言うと、以下の3つのリソースを修正しました。

- API gateway
- Cloudfront
- S3

#### API gateway

``` yml
  Api:
    Cors:
      AllowOrigin: "'*'"
      AllowHeaders: "'Origin, Authorization, Accept, X-Requested-With, Content-Type, x-amz-date, X-Amz-Security-Token'"
      AllowMethods: "'POST, GET, OPTIONS, DELETE, PATCH, PUT'"
      AllowCredentials: "'true'"
```

AllowOriginについては、ローカルからのアクセスもあるのでワイルドカードを残しました。検証したところ問題なく動作したのでここはワイルドカードでも良さそうです。

まだ自分でも精査できていないのですが、この記事を書いている最中に[MDN Web Docs](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Headers)から **Headersの一部は常に許可されているので記述する必要がない** という記載もみつけたので、もう少し記述を減らせそうですね。

#### Cloudfront
CloudfrontのWhitelistにAuthorizationを追加。

#### S3
S3のCORSの設定も明示的に記述します。

``` html
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://*</AllowedOrigin>
    <AllowedOrigin>http://*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Origin</AllowedHeader>
    <AllowedHeader>X-Requested-With</AllowedHeader>
    <AllowedHeader>Content-Type</AllowedHeader>
    <AllowedHeader>Accept</AllowedHeader>
    <AllowedHeader>Authorization</AllowedHeader>
    <AllowedHeader>x-amz-*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```

少し荒削りなところもありますが、以上でiOS12系以下でのCORSエラーを解消することができ、APIを利用することができるようになりました。

## 最後に
CORSのエラーは開発者からすると悩みの種の一つのように感じますが、今回なんとか解決することができて良かったと思います。ワイルドカードは便利に思われがちですが、こう言った場面で足元をすくわれることもあるんだなという良い経験になったように感じます。

状態的にはとりあえず動く、といったところなので動作検証しつつ精査をしようと思います。
でも検証のために毎回プッシュしてデプロイしなきゃいけないので、コミットが少し汚れ気味……。

ではまた。

## 参照
[MDN Web Docs(Access-Control-Allow-Headers)](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Headers)
[プロキシ統合用の Lambda 関数の出力形式](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format)