SPAだとCloudFront Functionsでレスポンスヘッダー付与できない！？

## はじめに
[Japan AWS Jr. Champions Advent Calendar](https://qiita.com/advent-calendar/2023/jr-champions) 12日目の投稿です！
今回はSPA（シングルページアプリケーション）でのCloudFront FunctionsでCloudFrontからのレスポンスヘッダー付与で起きた問題、解決策について紹介します。

## 起きた問題
タイトル通りSPAアプリケーションをCloudFront・S3で配信してる時にCloudFront Functionsでレスポンスヘッダーを付与されていなかったという問題がありました。
さらにセキュリティヘッダーがルートパスアクセス時のみ付与されており、ルートパス以外はセキュリティヘッダーがレスポンスに含まれていない状況でした。

### 構成
下記が簡単な構成図です。
SPAアプリケーションをS3・CloudFrontで配信しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/eab86d2c-e472-abd9-522c-14a38d7a430c.png)
CloudFrontからレスポンスを返す際にCloudFunctionsでセキュリティヘッダーを付与して返すようにしていました。

```js
function handler(event) {
    var response = event.response;
    var headers = response.headers;

    // Set HTTP security headers
    // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation 
    headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; 
    headers['content-security-policy'] = { value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; frame-ancestors 'none'"}; 
    headers['x-content-type-options'] = { value: 'nosniff'}; 
    headers['x-frame-options'] = {value: 'DENY'}; 
    headers['x-xss-protection'] = {value: '1; mode=block'};
    headers['referrer-policy'] = {value: 'same-origin'};
    
    // Return the response to viewers 
    return response;
}
```
※[amazon-cloudfront-functions
](https://github.com/aws-samples/amazon-cloudfront-functions/blob/main/add-security-headers/index.js
)

### ルートパスでアクセスした際のレスポンス
```shell
$curl --verbose https://xxxxxxxx.cloudfront.net  
< HTTP/2 200
< content-type: text/html
< content-length: 990
< date: Sat, 09 Dec 2023 05:51:43 GMT
< last-modified: Sat, 09 Dec 2023 04:38:05 GMT
< etag: "5f0bac55f798ed9bd5de8f36e707607d"
< x-amz-server-side-encryption: AES256
< accept-ranges: bytes
< server: AmazonS3
< via: 1.1 e8888b4ce0d0032a21220ed1f337571c.cloudfront.net (CloudFront)
< age: 1147
< content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; frame-ancestors 'none'
< referrer-policy: same-origin
< strict-transport-security: max-age=63072000; includeSubdomains; preload
< x-content-type-options: nosniff
< x-frame-options: DENY
< x-xss-protection: 1; mode=block
< x-cache: Hit from cloudfront
< x-amz-cf-pop: NRT20-P1
< x-amz-cf-id: gWhJacYoKccYL-ljoQDGPKIKIjjurwOlPLzWvE8G2aeH16-I_K4-nA==

```

### ルートパス以外でアクセスした場合のレスポンス
```shell
$curl --verbose https://xxxxxxxx.cloudfront.net  
< HTTP/2 200
< content-type: text/html
< content-length: 990
< date: Sat, 09 Dec 2023 05:51:43 GMT
< last-modified: Sat, 09 Dec 2023 04:38:05 GMT
< etag: "5f0bac55f798ed9bd5de8f36e707607d"
< x-amz-server-side-encryption: AES256
< accept-ranges: bytes
< server: AmazonS3
< x-cache: Error from cloudfront
< via: 1.1 eb2281d04aecdff9b5230922e2a3cec6.cloudfront.net (CloudFront)
< x-amz-cf-pop: NRT20-P1
< x-amz-cf-id: vO0h02QNUeLreBwtw14gEQ1_Kgb9g0Fx8MP3G3QcXaVS_hD_DfH95g==
< age: 1673
```
上記のようにパスを追加してリクエストするとレスポンスにCloudFront Functionsで付与さえるはずの`strict-transport-security`などのレスポンスヘッダがありません。

## 原因
SPAをCloudFront・S3で配信する時にオリジンからのレスポンスで400系のエラーが返されると、CloudFront Functionsが実行されないためにレスポンスヘッダーが付与されていませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/4a5c49fa-b157-46ac-bfb9-945f39eff8d3.png)

公式ドキュメントにも記載があります。
>CloudFront does not invoke edge functions for viewer response events when the origin returns HTTP status code 400 or higher.

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-status-codes

### なぜルートパスとルートパス以外で違いが出たのか
CloudFrontのルートパスにアクセスした際は`index.html`を返す設定にしてあり、ルートパスの`/`でアクセスするとindex.htmlをステータスコード200で返すためCloudFront Functionsが実行されセキュリティヘッダーが付与されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/656f7689-2233-4277-e0d8-0b2f6a11c062.png)

SPAアプリケーションをS3・CloudFrontで配信する場合は、`/home`などでアクセスしてもS3上にそのファイルがなく403や404でレスポンスされエラーページを設定しておかないと下記のようなエラーになってしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/f640cf20-9750-c9a1-97f0-4a7cfff1b97b.png)

SPAアプリケーションをS3・CloudFrontから配信する場合は、下記のようにS3（オリジン）から403や404が返された場合にS3の`/index.html`を返すようにエラーページを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/a2ae3f29-a083-fb32-3ca4-b4423e2c5a88.png)


## 解決策
結論としてはCloudFrontのレスポンスヘッダーポリシーを活用しましょう。
既に用意されているものを使用するか、自分でもポリシーを作成することができカスタマイズ可能です。
CloudFrontからのViewerへのレスポンスに常にレスポンスヘッダーポリシーで付与されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/b139010f-9e0d-ff1a-78bb-59da31ceee9a.png)

以下はSecurityHeadersPolicyを使用した結果になります。
```shell
$curl --verbose https://xxxxxxxx.cloudfront.net  
< HTTP/2 200
< content-type: text/html
< content-length: 990
< date: Sat, 09 Dec 2023 05:51:43 GMT
< last-modified: Sat, 09 Dec 2023 04:38:05 GMT
< etag: "5f0bac55f798ed9bd5de8f36e707607d"
< x-amz-server-side-encryption: AES256
< accept-ranges: bytes
< server: AmazonS3
< x-cache: Hit from cloudfront
< via: 1.1 f76b4c0eb6c4658feb5d2183e218bcee.cloudfront.net (CloudFront)
< x-amz-cf-pop: NRT20-P1
< x-amz-cf-id: L9ECsNOCgMpw03bFLVcQPcgYK2QipcOvqF59gKGVoVE1ebt0pS28xg==
< age: 2666
< x-xss-protection: 1; mode=block
< x-frame-options: SAMEORIGIN
< referrer-policy: strict-origin-when-cross-origin
< x-content-type-options: nosniff
< strict-transport-security: max-age=31536000
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/d02488d8-4b85-5406-e57d-34c47ac58231.png)


## まとめ
CloudFrontのレスポンスヘッダーポリシーは[2021年のアップデート](https://aws.amazon.com/jp/about-aws/whats-new/2021/11/amazon-cloudfront-supports-cors-security-custom-http-response-headers/
)で使用できるようになっています。
アップデート情報をしっかりと追えてなく解決までにやや時間がかかってしまったことへの自戒の念をこめて書かせていただきあました。
やはり情報を追って日々知識をアップデートして引き出しを増やしておくのは、解決までのスピードを早めていく上でとても大事だなと思いました。
re:inventでの情報もしっかりとキャッチアップしていきます！