AWSから行うセキュア動画配信 ~署名付きCookieを利用したプライベートコンテンツ配信~

## はじめに

AWSから行うセキュア動画配信の後編の投稿になります。

* [前編](https://qiita.com/r-mizuno/items/79ec52ca77ac0bb1b503)：　配信するための動画を、AWS Key Management System (KMS) から生成した共通鍵で暗号化し、S3へ格納する
* [中編](https://qiita.com/r-mizuno/items/b3ec1cdd4851d6b54823)：　AWS Elemental MediaConvert を利用して、HLS+AES暗号化の形へ動画を変換する
* 後編：　**Cloudfrontの署名付きCookieを利用して、アクセス可能なユーザを制限する(今回)**

前回は、S3へ格納された動画をMediaConvertを使用して、HLS+AESの形へ動画を変換しました。
今回は、変換した動画をCloudFrontから、署名付きCookieを利用して配信する手順をご紹介します。

## 構築予定の環境

構築予定のシステム構成図は以下のようになります。
本記事では、赤字となっている部分を解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/8dc3043f-78b6-9ae2-51b7-d738617c2b77.png)


## 作業手順
以下の手順でAWS上に動画変換の為の環境を設定していきます。

1. [CloudFrontの作成](#1-CloudFrontの作成)
2. [CloudFrontのOrigin追加（復号鍵格納用バケットの追加）](#2-CloudFrontのOrigin追加復号鍵格納用バケットの追加)
3. [動画参照確認](#3-動画参照確認)
4. [CloudFrontの署名付きCookieの設定](#4-CloudFrontの署名付きCookieの設定)
5. [SecretsManagerに秘密鍵を保存](#5-SecretsManagerに秘密鍵を保存)
6. [Cookieを作成するLambda関数を作成](#6-Cookieを作成するLambda関数を作成)
7. [API Gatewayを作成](#7-API-Gatewayを作成)
8. [CloudFront DistributionにAPI Gatewayのエンドポイントを追加](#8-CloudFront-DistributionにAPI-Gatewayのエンドポイントを追加)
9. [動作確認](#9-動作確認)


### 1. CloudFrontの作成

まずはCloudFrontのDistributionを作成します。
以下の画像のように入力してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/786093c1-9571-7995-2c62-35eb7f376d80.png)


* Origin Domain Name： 中編で作成した、HLS動画格納用のS3バケットを指定。
* Origin Id: Origin Domain Nameを入力すると自動で入力されるのでそのままとする。
* Restrict Bucket Access: Yes
* Origin Access Identity: Create a New Identity
* Grant Read Permissions on Bucket: Yes, Update Bucket Policy
* Viewer Protocol Policy: Redirect HTTP to HTTPS

入力でき次第、ページ下部の[Create Distribution]を押下します。

### 2. CloudFrontのOrigin追加(復号鍵格納用バケットの追加)

1．で作成したDistributionはHLS用のS3のOriginは設定しか設定していないので、このままだと復号鍵を参照できません。
なので、復号鍵格納用S3バケットのOriginも追加してあげる必要があります。

先ほど作成したCloudFront Distributionを選択し、[Origins and Origin Groups]を選択します。
そこで[Create Origin]を押下し、以下のように入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/79f836e1-1700-8223-020a-f5cd3227f269.png)


1．で入力した内容とほぼ同じですが、Origin Domain Nameのみ、復号鍵格納用のS3バケットを指定する点に注意してください。

続いて、[Behaviors]タブに移動して[Create Behavior]を押下して以下のように入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/c71716e9-6f14-a97e-d0e5-dd70726029a9.png)


* Path Pattern: *.key
* Origin or Origin Group: 先ほど[Create Origin]にて作成したOriginを指定します。
* Viewer Protocol Policy: Redirect HTTP TO HTTPS

入力でき次第、ページ下部の[Create]を押下してBehaviorを作成します。
ここで行った設定により、ファイル名の末尾が.keyとなるファイルを参照した際は復号鍵格納用S3からファイルを取得するようになりました。

### 3. 動画参照確認

では、ここでCloudFrontを通して正常に動画が参照できるか確かめてみましょう。
以下のURLへアクセスしてみます。

```
https://<CloudFront domainname>/<HLS変換後のプレイリスト名>.m3u8
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/1d0aa87c-5764-5c6e-b2a8-fae9c9e22eaf.png)


正常に動画が再生できることが確認できました。

### 4. CloudFrontの署名付きCookieの設定

このままの状態だと誰でも動画を参照可能な状態となってしまっているので、特定のユーザしか見れないように設定を行っていきます。

#### キーグループ用のキーペアの生成

署名付きCookieを利用する場合、暗号化に使用するキーペアを作成し、それをCloudFrontのキーグループへ登録する必要があります。
以下のコマンド例を参考にキーペアを生成します。

```
$ openssl genrsa -out private_key.pem 2048
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
```

#### 公開鍵をCloudFrontのPublic Keyへ登録する

CloudFront メニューから [Key management] - [Public keys] を開き、[Add public key] をクリックして、以下を入力します。

* Key name: 任意の文字列。生成するキーの名前
* Key value: 生成したpublic_key.pemの中身。
* Comment: 任意。コメント

#### Public KeyをCloudFrontキーグループに追加する

CloudFront メニューから [Key management] - [Key groups] を開き、[Create key group] をクリックして以下を入力します。

* Key group name: 任意の文字列。生成するキーグループの名前。
* Comment: 任意。
* Public Keys: 作成したpublic keyを選択し、Addする

#### CloudFront Distributionの署名付きCookieの設定を有効化する

生成したキーグループを使用して署名付きCookieを利用するようにCloudFrontへ設定を行います。
CloudFrontメニューから [Distributions] - 署名付きCookieを設定するCloudFrontのDistributionを選択して[Distribution Settings]。
[Behaviors] から、すでに存在しているBehaviorを選択し、[Edit]。
以下のように設定し、[Yes,Edit]を押下します。

* Restrict Viewer Access : Yes
* Trusted Key Groups or Trusted Signer: Trusted Key Groups
* Trusted Key Groups: 3．で生成したキーグループを指定し、[Add]を押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/1c431f5c-f754-45c3-5f92-e883b14d8268.png)

1．及び2．で作成したBehaviorそれぞれに対して同じ設定を行う必要がある点に注意してください。

これにより、署名付きCookieを利用しないとコンテンツの参照が不可能となりました。
3．でアクセスしたURLへ再度アクセスすると、以下のような画面が表示されて動画が再生できないことが確認できると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/a6fc43b6-906c-82f2-a828-cb8ffc885e2e.png)

### 5. SecretsManagerに秘密鍵を保存

続いて、生成したキーペアの秘密鍵をAWS上で管理し、Cookie発行の為のLambdaから利用可能とするためSecretsManagerへ登録します。

SecretsManagerメニューから、[新しいシークレットを保存する]を押下します。
以下のように入力し、秘密鍵を保存します。


#### ステップ1: シークレットのタイプ

* シークレットの種類を選択： その他のシークレット
* このシークレットに格納するキーと値のペアを指定します： プレーンテキストを選択し、以下を入力します。
    
    ```
    {
        "private_key":"{4.で生成したprivate_key.pemの中身}"
    }
    ```
    **※以下の画像のようにprivate_key.pemの中身は、改行をすべて\nに置き換えて登録してください。**
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/3d76f501-d389-e71d-2500-bada16af856f.png)


* 暗号化キーを選択してください: DefaultEncryptionKey
    
#### ステップ2: 名前と説明
* シークレットの名前: 任意
* オプションの説明： 任意

#### ステップ3: ローテーションを設定する
* 自動ローテーションを無効にするにチェック

### 6. Cookieを作成するLambda関数を作成

Cookie発行の為のLambda関数を生成していきます！

以下の条件で関数を作成してください。

* ランタイム： Node.js 14.x
* 実行ロール: 基本的なLambdaアクセス権限に加え、SecretsManagerReadWrite ポリシーをアタッチしたもの。
* コード

``` javascript
const AWS = require("aws-sdk");

// public key id
const SIGNER_PUBLIC_KEY = "XXXXXXXXXXXXX"
// Sercret Managerに設定したシークレットの名前
const SECRET_ID = "XXXXX"
// ドメイン名
const DOMAIN = "xxxxxxxxxxxxxx.cloudfront.net"

const secretsManager = new AWS.SecretsManager();

exports.handler = async (event) => {
    // queryから参照許可対象となるファイル名を取得
    let filename = event.queryStringParameters.filename
    if (filename && filename.includes('.')) {
        filename = filename.split('.')[0]
    } else {
        // 未入力の場合、400エラーを返却
        return {
            'statusCode': 400,
            body: JSON.stringify({
                message: "Bad request. missing request parameter: filename."
            })
        }
    }
    // SecretsManagerから鍵情報の取得
    const response = await secretsManager.getSecretValue({
        SecretId: SECRET_ID
    }).promise();
    const private_key = JSON.parse(response.SecretString).private_key;

    // 有効期限の設定(UnixTime)
    let expiredTime = new Date()
    // 24時間後
    expiredTime.setHours(expiredTime.getHours()+24)
    // ポリシーステートメントの作成
    const policy = {
        Statement: [
            {
                Resource: `https://${DOMAIN}/${filename}*`,
                Condition: {
                    DateLessThan: {
                        "AWS:EpochTime": Math.floor(expiredTime / 1000)
                    }
                }
            }
        ]
    };
    const signer = new AWS.CloudFront.Signer(
        SIGNER_PUBLIC_KEY,
        private_key
    );
    const signedCookie = signer.getSignedCookie({
        policy: JSON.stringify(policy)
    });

    const result = {
        'statusCode': 200,
        'headers':{
            'Content-Type': 'application/json; charset=utf-8',
            "Access-Control-Allow-Origin": `https://${DOMAIN}`,
            "Access-Control-Allow-Credentials": "true",
            "Access-Control-Allow-Headers": "*"
        },
        body: JSON.stringify({
            message: "success!"
        }),
        multiValueHeaders: {
            "Set-Cookie": [
                `CloudFront-Policy=${signedCookie["CloudFront-Policy"]};Domain=${DOMAIN};path=/;`,
                `CloudFront-Signature=${signedCookie["CloudFront-Signature"]};Domain=${DOMAIN};path=/;`,
                `CloudFront-Key-Pair-Id=${signedCookie["CloudFront-Key-Pair-Id"]};Domain=${DOMAIN};path=/;` 
            ]
        }
    }
    return result;
};
```
    
また、以下の変数を環境に合わせた値に書き換えてください。

* SIGNER_PUBLIC_KEY： 4. CloudFrontの署名付きCookieの設定で作成したPublic KeyのID
* SECRET_ID: Sercret Managerに設定したシークレットの名前
* DOMAIN: 署名付きCookieを利用するCloudFrontドメイン名


### 7. API Gatewayを作成

作成したLambda関数を実行し、署名付きCookieを取得するためのAPI Gatewayを作成します。

API Gatewayのメニューから、[API作成]を選択し、REST APIの[構築]を押下します。
API名に任意の名前を入力し、[APIの作成]を押下してAPIを作成します。

#### リソースの作成

作成できたら、以下の画像のようにリソースを作成していきます。
[アクション] → [リソースの作成]の順に選択し、リソース名、リソースパスを入力し、[リソースの作成]を押下します。
ここでは例としてcookiesを指定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/03e571e1-fce7-13dd-fb56-f61cf5e6a337.png)


#### メソッドの作成

次に、先ほど作成したリソースの/cookiesを選択し、[アクション]→[メソッドの作成]→[GET]の順に指定します。
methodのセットアップ画面が表示されるので、以下のように入力していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/e3a2e33c-f09d-f9e9-6303-1987e9387f0d.png)

* 統合タイプ: Lambda関数
* Lambdaプロキシ統合の使用: チェックを入れる
* Lambda関数:  6. Cookieを作成するLambda関数を作成 で作成したLambda関数を指定

#### メソッドリクエストの設定

メソッドの作成が完了したら、クエリ文字列パラメータのバリデーション設定を行います。

[メソッドリクエスト]を選択すると以下のような画面が表示されるので、必要事項を入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/ce272d1a-9831-2d64-434a-7541362ccc09.png)

* リクエストの検証： クエリ文字列パラメータ及びヘッダーの検証
* URLクエリ文字列パラメータ
    * 名前： filename
    * 必須： チェックを入れる

この設定により、クエリパラメータにfilenameを指定しないとエラーを返却するようになります。

#### APIのデプロイ

ここまでの設定が完了したら、APIを実行可能にするためにデプロイを行います。
[アクション] → [APIのデプロイ] を選択すると、以下の画面が表示されます。
デプロイされるステージに新しいステージを指定し、任意のステージ名を入力してデプロイします。
ここではsampleと指定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/fd9f2add-d25c-86d6-b4c4-4f07faa6d598.png)

デプロイが完了すると、ステージから以下のようにAPIの呼び出し先が確認できます。
次の章で設定に使用するので、メモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/10cc1d9b-22eb-08f4-46d4-0bcacf79952f.png)




### 8. CloudFront DistributionにAPI Gatewayのエンドポイントを追加

デプロイしたAPIをCloudFrontを経由して実行可能とするため、Origin及びBehaviorを設定します。

#### Originの作成
まず、Originを作成していきます。
CloudFrontメニューから、作成したCloudFront Distributionを選択して[Origins and Origin Groups]タブ → [Create Origin]を押下します。
以下のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/fbdc129d-e53b-8532-ced3-ce813a0d8031.png)

* Origins Domain Name:  APIのデプロイ時に確認した、APIの呼び出し先のドメインを指定します。**この際、ステージ名は入力せず、ドメイン名のみとしてください。**
* Minimum Origin SSL Protocol: TLSv1.2
* Origin Protocol Policy: HTTPS Only

#### Behaviorの作成
続いて、Behaviorの作成を行います。
[Behaviors]タブ → [Create Behavior]を押下し、以下のように入力を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/5beb842f-b94c-6565-47b8-4caf4472b4d9.png)



* Path Pattern: デプロイしたAPIのステージ名より下位のパスを指定します。ここでは例として「sample\/\*」を指定しています。
* Origin or Origin Group: 先ほど作成したOriginを指定します。
* Cache and origin request settings: Use Legacy cache settings
* Object Caching: Customize
* Maximum TTL: 0を指定。キャッシュしない設定とします。
* Default TTL: 0を指定。同上。
* Forward Cookies: ALLを指定。ここでCookieを転送する設定としておかないと、ブラウザ側にCookieが転送されません。
* Query String Forwarding and Caching: Forward all, cache based on whitelist
* Query String Whitelist: filenameを指定。クエリの転送を許可しておきます。

これにより、動画ファイルと同じドメインで署名付きCookie発行APIが実行できるようになりました！

### 9. 動作確認

署名付きCookieを使用して動画を再生する準備ができたので、実際に動作確認をしていきます。

まず、ブラウザから以下のURLへアクセスし、APIを実行します。

```
https://<cloudfront domainname>/sample/cookies?filename=<HLS変換前の動画ファイル名>.mp4
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/09deb0f5-c7e1-62d1-17c2-6383b3d177be.png)

すると上記のような画面が表示されました。正常にレスポンスが返却されているようです。
開発者ツールからCookieを確認してみると、Cookieがセットされていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/716afff0-61b0-f92f-9739-6f5f2b104d91.png)

この状態で[3. 動画参照確認](#3-動画参照確認)でアクセスしたURLへ再度アクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/a878783a-fb31-0766-7daa-0a6c5f6dde7a.png)


署名付きCookieを利用して動画にアクセスすることができました！

## 終わりに

AWSからセキュアな環境で動画配信する手順を3回に分けてお送りしました。
本記事で記載した内容は一例の為API Gatewayに認証をかける手順は割愛しましたが、Cognitoによる認証トークンの指定を必須とするなどの仕組みを導入してあげると、SPAなどからAPIを呼び出して認証済みのユーザのみCookieを発行するといったよりセキュアな環境が実現可能です。
本記事がどなたかのお役に立てば幸いです。

## 参考
コードの内容など、以下の記事を参考にさせていただきました。(2021年3末時点の情報) 
https://dev.classmethod.jp/articles/get-hls-using-signedcookie/