AWS S3にCognitoの認証を用いてモバイルアプリからアップロード

# やりたい事
AndroidやiOSなどのモバイルアプリケーションからデータをクラウド上にアップロードしたい場合があるかと思います。以下のような要件をみたすアーキテクチャーを実現したいと考えています。

* データをアップロードするS3のバケットは公開されいない。保存されたデータも公開されない。
* IAMのアクセスキーをアプリケーションにハードコーディングするといった、セキュリティ上問題がある事は行わない。

上記を実現するためにCognitoを用いて、Androidアプリケーションに一時的なクレデンシャルを与えて、許可されたS3のバケットにファイルをアップデートする仕組みを作ります。

# S3バケットの準備
データアップロード様のS3バケットを準備します。このバケットは、中に格納されるオブジェクトが謝って公開されない様に、バケットのパブリックアクセスコントロールリスト（ACL)を全て設定した状態で作成します。

![スクリーンショット 2019-03-16 15.41.03.png](https://qiita-image-store.s3.amazonaws.com/0/85182/71f21996-742b-79a8-89b5-334695fd7d80.png)

上記の設定をして置くと、バケットの中のオブジェクトを公開しようとしても、下記の様にエラーになります。バケットのACLの方がオブジェクトのACLより優先されるという事かと思います。

![スクリーンショット 2019-03-16 15.49.20.png](https://qiita-image-store.s3.amazonaws.com/0/85182/7c49d0e3-bfb3-12b9-7f1e-ecc41b987804.png)

アプリケーションからのデータは一定期間だけ保存すれば良い場合は、バケットに対してライフサイクルを設定する事ができます。
この例では、90日以上経過したデータは削除する設定にしています。ライフサイクルのルールは最大1000個まで設定可能な模様です。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/intro-lifecycle-rules.html

![スクリーンショット 2019-03-16 15.56.44.png](https://qiita-image-store.s3.amazonaws.com/0/85182/92dcef06-fd96-e94e-51cd-c89c00ed55af.png)

![スクリーンショット 2019-03-16 16.02.30.png](https://qiita-image-store.s3.amazonaws.com/0/85182/e4b1b925-c673-cb73-c647-8555d4b201bf.png)

# Cognitoの設定
CognitoのIDプールを作成します。Cognitoは通常、FacebookやGoogleなどのユーザー認証を簡単に実現する事ができますが、今回はアプリケーションでログインさせるのではなく、認証なしでアプリケーションにS3の権限を与えます。よって認証プロバイダーは設定しません。

![スクリーンショット 2019-03-16 16.21.31.png](https://qiita-image-store.s3.amazonaws.com/0/85182/1726f70c-d7a2-29cb-7e41-1a9fc0967090.png)

次にロールの設定で、先ほど作成したS3のバケットに権限を与えます。unauthenticated identitiesに対してロールを作成し、以下の様な設定を行います。

![スクリーンショット 2019-03-16 16.23.06.png](https://qiita-image-store.s3.amazonaws.com/0/85182/53fb73d5-bc6c-8662-cc94-78a9043ab4a6.png)

以下のポリシーでは、my-mobile-app-dataバケット以下のオブジェクトに対して全ての権限が与えられています。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*"
      ],
      "Resource": [
        "*"
      ]
    },
    {
        "Effect": "Allow",
        "Action": [
            "s3:*Object"
        ],
        "Resource": [
            "arn:aws:s3:::my-mobile-app-data/*"
        ]
    }
  ]
}
```

IDプールを作成すると、以下の様にAndroidで実装するためのサンプルを表示してくれます。

![スクリーンショット 2019-03-16 16.32.14.png](https://qiita-image-store.s3.amazonaws.com/0/85182/64e1a29a-a20d-481e-5dc2-22f94de82df5.png)

# Androidアプリの実装
AWSのSDKを利用しますので、project直下のbuild.gradleにクラスパスを追加します。

```gradle
dependencies {
    classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.+'
```
次に必要となる、ライブラリをapp/build.gradleに設定します。

```gradle
dependencies {
    implementation 'com.amazonaws:aws-android-sdk-core:2.+'
    implementation 'com.amazonaws:aws-android-sdk-cognito:2.+'
    implementation 'com.amazonaws:aws-android-sdk-s3:2.+'
```
この例では/sdcard直下のtest.jpegをアップロードしますので、インターネット関連のパーミッションに加えて、外部ストレージアクセスのパーミッションを設定します。パーミッションだけではうまく行かない場合があるので、Androidの設定画面のストレージから作成したアプリにアクセス権を与えて下さい。

```xml
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```
以下、簡単にプログラムを書きました。シンプルにするためにMain ActivityのonCreateの中でAsyncTaskを作って、その中でS3のバケットにアップデートします。Cognitoで一時的なクレデンシャルを取得して、その後にS3 Clientを作成してファイルをアップロードしています。 以下のコードでS3のバケットにアップロードされる事を確認しました。

```java
    private static class FileUploadTask extends AsyncTask <Integer, Integer, Integer> {
        private static final String BUCKET = "my-mobile-app-data";
        private static final String TEST_FILE = Environment.getExternalStorageDirectory().getPath() + "/test.jpeg";

        private final Context mContext;
        private FileUploadTask(Context context) {
            mContext = context;
        }

        @Override
        protected Integer doInBackground(Integer... value) {
            CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
                    mContext,
                    <ID Pool>, // ID プールの ID
                    Regions.AP_NORTHEAST_1 // リージョン
            );

            AmazonS3Client s3 = new AmazonS3Client(credentialsProvider, Region.getRegion(Regions.AP_NORTHEAST_1));
            TransferUtility transferUtility = TransferUtility.builder().s3Client(s3).context(mContext).build();
            File file = new File(TEST_FILE);
            TransferObserver transferObserver = transferUtility.upload(BUCKET, "test.jpeg", file);
            transferObserver.setTransferListener(new TransferListener() {
                @Override
                public void onStateChanged(int id, TransferState state) {
                }

                @Override
                public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
                }

                @Override
                public void onError(int id, Exception ex) {
                }
            });
            return 0;
        }

        @Override
        protected void onPostExecute(Integer result) {
        }
    }
```

<img width="1055" alt="スクリーンショット 2019-03-16 21.43.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/85182/10b901a3-baac-b3c1-33d6-1cb4b53b28e0.png">