Unity から AWS を使う (Cognito で認証して S3 の署名付き URL を作成する)

# Unity で AWS を使いたい !

Unity で S3 に保存している画像をログインできた人だけに見れるようにしたい！
というような場合の、
AWS SDK のインストールから、Cognito 認証、認証情報を使った S3 の署名付き URL 作成の仕方を書き留めます。

※ AWS で Cognito、S3 を作るやり方については書いていません :bow:

## バージョン情報

- Unity: 2022.3.2f.1
- AWS SDK .NET Standard 2.0: 3.7.300.16

## 1. AWS SDK を Unity に入れる

[Special considerations for Unity support](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/unity-special.html) には DLL を手動でダウンロードし、ディレクトリに配置する手順が書かれていますが、[Nuget For Unity](https://github.com/GlitchEnzo/NuGetForUnity) でもできます！
今回は Nuget を使った手順を紹介します。

### 1.1. NugetForUnity のインストール

`Window` > `Package Manager` を開いて、`Packages` で `Unity Registry` を選択、検索ボックスから `NugetForUnity` を検索して Install します。

![スクリーンショット 2023-12-12 20.42.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599504/f10d5e5a-1aa4-ba0a-b28f-90872415bb8b.png)


### 1.2. AWS SDK のインストール

`NuGet` > `Manage Nuget Packages` を開いて `Online` を選択し、検索ボックスを使って、パッケージを Install します。

![スクリーンショット 2023-12-12 20.51.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599504/84bf910d-6fb0-5b44-5245-7f15e963c619.png)

今回は Cognito と S3 を使うので、以下を Install します。

- AWSSDK.Core
- AWSSDK.CognitoIdentity
- AWSSDK.CognitoIdentityProvider
- AWSSDK.SecurityToken
- AWSSDK.S3
- Amazon.Extensions.CognitoAuthentication

手で入れる場合は、依存する `Microsoft.Bcl.AsyncInterfaces.dll` とかもダウンロードしてくる必要がありますが、`Nuget` であれば、依存するパッケージも同時に入れてくれます。


### 1.3. link.xml を作成

ビルドに [IL2CPP](https://docs.unity3d.com/Manual/IL2CPP.html) を使う場合は、`Assets` フォルダに、以下を記載した `link.xml` ファイルを作成します。
これをしないと、メソッドが取り除かれてしまい `MissingMethodException` が出ることがあります。

```xml
<linker>
  <assembly fullname="AWSSDK.Core" preserve="all"/>
  <assembly fullname="AWSSDK.CognitoIdentity" preserve="all"/>
  <assembly fullname="AWSSDK.CognitoIdentityProvider" preserve="all"/>
  <assembly fullname="AWSSDK.SecurityToken" preserve="all"/>
  <assembly fullname="AWSSDK.S3" preserve="all"/>
  <assembly fullname="Amazon.Extensions.CognitoAuthentication" preserve="all"/>
</linker>
```


## 2. ログイン機能をつくる

### 2.1. Cognito 認証のメソッドを実装

Cognito を使うために、以下の情報が必要ですので、AWS で Cognito AWS のコンソールで調べておいてください。

- CognitoPoolId
- CognitoClientId
- CognitoIdentityPoolId

[CognitoIdentityProviderClient](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/CognitoIdentityProvider/TCognitoIdentityProviderClient.html) を使って認証を実施します。

```c#
using System.Threading.Tasks;
using UnityEngine;
using Amazon;
using Amazon.CognitoIdentity;
using Amazon.CognitoIdentityProvider;
using Amazon.Extensions.CognitoAuthentication;
using Amazon.Runtime;
using Amazon.S3;

public class AwsUtils : MonoBehaviour
{
    const string CognitoPoolId = "ap-northeast-1_xxxxxxxxx";
    const string CognitoClientId = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
    const string CognitoIdentityPoolId = "ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";

    private static CognitoAWSCredentials CognitoCredentials;
    private static CognitoUser cognitoUser = null;

    // ユーザ名とパスワードを入力して、認証を実施
    public static async Task<AuthFlowResponse> CognitoAuth(string username, string password)
    {
        AmazonCognitoIdentityProviderClient provider = new(new AnonymousAWSCredentials());
        CognitoUserPool userPool = new(CognitoPoolId, CognitoClientId, provider);
        cognitoUser = new(username, CognitoClientId, userPool, provider);
        InitiateSrpAuthRequest authRequest = new()
        {
            Password = password
        };

        AuthFlowResponse authResponse = await cognitoUser.StartWithSrpAuthAsync(authRequest).ConfigureAwait(false);

        // 認証情報を取得
        CognitoCredentials = cognitoUser.GetCognitoAWSCredentials(CognitoIdentityPoolId, RegionEndpoint.APNortheast1);

        return authResponse;
    }
}
```

※ リージョンは `ap-northeast-1` を想定しています。


この後、取得した認証情報 `CognitoCredentials` を使って色々な AWS リソースを利用します。

## 2.2. ログイン画面をつくる

`UI` > `Input Field - TextMeshPro` でユーザ名とパスワードの入力フォーム `UI` > `Button - TextMeshPro` でログインボタンを作ります。

![スクリーンショット 2023-12-12 23.51.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599504/fbaef99a-20f0-e2d8-a012-9f2bcd1bcb1d.png)

※ メールアドレスをユーザ名として使っています

## 2.3. ログインスクリプトを作成

`username` と `password` を `InputField` から取得し、ログインを試みます。
以下のようあスクリプトを作成して、`LoginPanel` に追加します。

```c#
using UnityEngine;
using TMPro;
using Amazon.CognitoIdentityProvider.Model;
using Amazon.Extensions.CognitoAuthentication;

public class LoginManager : MonoBehaviour
{
    [SerializeField] private TMP_InputField emailField;
    [SerializeField] private TMP_InputField passwordField;

    // ログイン成功後に実施する
    public Action OnLogin { get; set; };

    public async void Login()
    {
        AuthFlowResponse authResponse;

        // 過去のアクセストークンを消しておく
        PlayerPrefs.DeleteKey("AwsAccessToken");
        
        try
        {
            authResponse = await AwsUtils.CognitoAuth(emailField.text, passwordField.text);
        }
        catch (UserNotFoundException e)
        {
            // ユーザが見つからない
            Debug.Log(e.Message);
            return;
        }
        catch (NotAuthorizedException e)
        {
            // 認証失敗
            Debug.Log(e.Message);
            return;
        }

        // アクセストークンを保存しておく
        PlayerPrefs.SetString("AwsAccessToken", authResponse.AuthenticationResult.AccessToken);

        // ログイン後のアクション
        OnLogin?.Invoke();
    }
}
```

※ 今回、アクセストークンは使っていませんが、使う場合もあるので保存しています。

`emailField` と `passwordField` に対応する `InputField` を設定し、
ログイン関数をログイン `Button` の `On Click` イベントに登録するとログイン機能の完成です！


## 3. 認証情報を使って AWS リソース (S3) を利用する

### 3.1. S3 の署名つき URL 作成メソッドを実装する

2.1. で作った `CognitoCredentials` を元に [S3Client](https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/S3/TS3Client.html) を使って S3 オブジェクトの署名付き URL を生成するメソッドを以下に示します。

(コードは 2.1 の続きです)
```c#
public class AwsUtils : MonoBehaviour
{
    ...

    // バケット名とオブジェクトキーを入力して、署名つき URL を生成
    public static string GeneratePresignedUrl(string bucketName, string objectKey, double duration = 900)
    {
        // 認証済みチェック
        if (CognitoCredentials == null) throw new Exception("Not logged in");

        var s3Client = new AmazonS3Client(CognitoCredentials);
        var request = new GetPreSignedUrlRequest()
        {
            BucketName = bucketName,
            Key = objectKey,
            Expires = DateTime.UtcNow.AddSeconds(duration),
        };
        
        return s3Client.GetPreSignedURL(request);
    }
```

※ S3 へのアクセス権が認証ユーザに付与されている必要があります。

### 3.2. URL を使って画像を貼るスクリプトを作成

例えば、S3 に保存したオブジェクトが画像の場合、取得した署名付き URL と [UnityWebRequestTexture](https://docs.unity3d.com/ja/current/ScriptReference/Networking.UnityWebRequestTexture.GetTexture.html) を使って、画像をテクスチャにして表示することができます。

```c#
public class ImageFetcher : MonoBehaviour
{
    [SerializeField] private LoginPanel loginPanel;
        
    const string bucketName = "xxxxx";
    const string objectKey = "xxxxx"
    
    Renderer imageRenderer;

    void Awake()
    {
        imageRenderer = gameObject.GetComponent<Renderer>();
    }

    void Start()
    {
        // ログイン後に画像を取得
        loginPanel.OnLogin += FetchAwsImage;
    }

    void FetchAwsImage()
    {
        // 署名付き URL を生成
        string url =  AwsUtils.GeneratePresignedUrl(bucketName, objectKey);
        // URL から画像を取ってきてテクスチャーに貼る
        StartCoroutine(FetchTexture(url));
    }

    // URL を指定して画像を Texture に貼る
    IEnumerator FetchTexture(string url)
    {
        using UnityWebRequest request = UnityWebRequestTexture.GetTexture(url);

        yield return request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            // 取得失敗
            Debug.LogError(request.error);
            yield break;
        }

        // 取得した Texture
        Texture2D texture = ((DownloadHandlerTexture)request.downloadHandler).texture;

        // Texture を貼り付け
        imageRenderer.material.mainTexture = texture;
    }
}
```

このスクリプトを `Quad` オブジェクトに追加して、`loginPanel` に 2.2. , 2.3. で作成したログイン画面を指定してあげると、ログイン後に S3 から画像を取ってきて、表示できます。

以上で、S3 に置いたファイルを Cognito 認証して取得する、ということができました！

## ちなみに

videoPlayer の URL に対して S3 の署名付き URL は現状使えないようです :cry:

https://forum.unity.com/threads/s3-presigned-url-in-videoplayer-url-does-not-play.1458070/#post-9137311