Netflix, Long an AWS Customer, Tests Waters on Google Cloud

Netflix, Long an AWS Customer, Tests Waters on Google Cloud — The Information. Netflix, one of Amazon Web Services’ biggest customers, is expanding its use of Google Cloud, AWS’s biggest rival, according to two people with knowledge of the matter. The move, which hasn’t previous. 続きを読む

Access Your Reserved Instance (RI) Coverage Information via the AWS Cost Explorer API

Access Your Reserved Instance (RI) Coverage Information via the AWS Cost Explorer API. RI coverage tracks the number of running instance hours that are covered by RIs, which allows you… 続きを表示. RI coverage tracks the number of running instance hours that are covered by RIs, which allows you … 続きを読む

UnityからAWS Cognito Identity Providerで認証機能を実装する(User_Auth_Flow)

前置き:AWSSDK for Unityでは現状,公式にCognito Identity Providerの機能をサポートしていません.紹介した方法は自己責任で参考にしてください.

UnityからCognito User Poolを使って認証する

この記事ではAWSのCognito User Poolを使用してログイン機能をUnityのアプリケーションに実装し,AWSのリソースにアクセスするためのトークンを取得する方法を紹介します.

Cognito User Poolとは

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html

自分でサーバー側の実装を書くことなくユーザー認証機能を実装できます.

手順

環境

AWSの設定

User Poolの作成と設定

UserPool.png

Cognitoからユーザーを管理するためのUser Poolを作成し,以下のように設定します.

  • プール名

    • わかりやすい任意の名前を設定します.
  • 属性
    • 標準属性のチェックを全て外し,ユーザー名とパスワードだけでログインできるように設定します.
  • アプリクライアントの追加

    • “アプリクライアントの追加”をクリックします.
    • アプリクライアント名を設定します.
    • “クライアントシークレットを生成する”をオフにします.

設定し終わったら”確認タブ”からプール作成を終了し,

  • プールIDとプール名 (リージョン名と固有なプール名の組み合わせ)

    • 例) ap-northeast-1_Hoge0Hoge
      を確認します.

さらにアプリの統合>アプリクライアントの設定タブから,

  • “有効なIDプロバイダ”のチェック欄に表示されている”Cognito User Pool”にチェックを入れます.
  • アプリクライアントID (25桁のAlphanumericな値)を確認します.

Identity Poolの作成と設定

IdentityPool.png

Cognitoから”フェデレーテッドアイデンティティ”のページに移動し,”新しいIDプールの作成”をクリックするとIDプール作成ウィザードが開始します.

  • IDプール名を入力します.
  • “認証されていないIDに対してアクセスを有効にする”をオンにするとゲストユーザーとしてアプリを使用することが可能です.ここでは必要ないのでオフのままで大丈夫です.
  • 認証プロバイダーのセクションで”Cognito”タブをクリックし,先ほど確認したユーザープールのIDとアプリクライアントIDを入力します.

Createpolicy.png

“プールの作成”をクリックすると,”AWSの諸々にアクセスするための権限の設定をする必要がある”ということで,ゲストユーザーと認証ユーザーの二つのユーザーについてIAMロールを設定するように言われます.

ここではどちらも”新しいIAMロールの作成”を選択し,そのまま右下の許可を押します.ここで作成した二つのIAMロール(Cognito_*Hoge*Auth_RoleCognito_*Hoge*Unauth_Role)はあとで編集するので名前を覚えておきます.

IAMロールの編集

先ほど作成したロールの権限を編集し,Cognito User Poolに対して認証リクエストを送る権限を付与します.

CogitoIdentityProvider.dll for Unityのビルド

AWS Mobile SDK for UnityではCognito User Poolがサポートされておらず,自分でCognitoIdentityProvider.dllをビルドする必要があります.dotNet版のコードジェネレーターの設定をいじることでUnityに対応したdllを得ることができます.

GitHubのissueに具体的な方法が書かれており,それを翻訳すると以下のようになります.

  1. AWS SDK dot netのリポジトリをクローンします.

  2. aws-sdk-netgeneratorServiceModelscognito-idpmetadata.jsonを編集し,"platforms": ["Unity"],をjsonのrootに追加します.

  3. aws-sdk-netgeneratorServiceClientGeneratorLib/Generators/ProjectFiles/UnityProjectFile.csの265行目でUnityEngine.dllのパスを修正します. UnityEngine.dllの場所はMacOSとWindowsで異なるので注意.ここではWindowsでビルドします.修正後のパスはC:/Program Files/Unity/Editor/Data/Managed/UnityEngine.dllとなります.

  4. aws-sdk-netgeneratorAWSSDKGenerator.slnをVisual Studioで開きます.実行ボタン(上の緑の三角ボタン)を押すとビルドされたコード生成プログラムが実行され,上手くいけばaws-sdk-netsdksrcServicesCognitoIdentityProvider内部にAWSSDK.CognitoIdentityProvider.Unity.csprojというファイルが生成されます.

  5. 次にaws-sdk-netsdkAWSSDK.Unity.slnを開き,Build TypeをReleaseに設定してからビルド(ctrl + shift + B)します.すると新しくaws-sdk-netsdksrcServicesCognitoIdentityProviderbinReleaseunityフォルダが作られ,中にCognitoIdentityProvider.dllが生成されます.

  6. UnityプロジェクトのAssets/下任意の場所にdllを置きます.

metadata.json
{
  "platforms": ["Unity"],
  "active": true, 
  "synopsis": "You can create a user pool in Amazon Cognito Identity to manage directories and users. You can authenticate a user to obtain tokens related to user identity and access policies. This API reference provides information about user pools in Amazon Cognito Identity, which is a new capability that is available as a beta."
}
UnityProjectFile.cs
...
 this.Write(this.ToStringHelper.ToStringWithCulture(Path.Combine((string)this.Session["UnityPath"], "Editor", "Data", "Managed", "UnityEngine.dll")));
...

Unityで認証フローを実装する

AWS Mobile SDK for Unityを公式サイトからダウンロードし,AWSSDK.IdentityManagement.unitypackageをプロジェクトにインポートします.

以下,ブログエントリーを参考にして認証フローと暗号化処理を実装します.

AdminInitiateAuth関数を使った方法が紹介されることが多いですが,パスワードが平文で送信されるのでモバイルアプリで用いるにはセキュリティ的に非常に危険です.今回はSecure Remote Password (SRP)プロトコルをつかった認証フローであるUSER_SRP_AUTHに従います.

暗号化の実装はブログエントリーが詳しく,また依存するHkdfクラスの実装もGistに公開されているものが完動するのでコピペします.Bouncy Castle C#のライブラリが必要なのでBouncy Castle C#のGitHubリポジトリから最新リリースをダウンロードし,.net2.0版のdllをプロジェクトにコピーします.

注意すべき点として, 作成したばかりのユーザーのステータスはFORCE_CHANGE_PASSWORDと設定されており,User Poolからのレスポンス(チャレンジ)として新しいパスワードを要求してきます.下のプログラムではまだ実装しておらず,パスワード変更済みのユーザーに対してログイン処理をかけています.

認証フローが成功すると
* IdToken
* AccessToken
* RefreshToken
の三つが手に入ります.

得られたIdトークンを使ってCredential.AddLogins(IdentityProviderName , IdToken)することで,GetCredentialForIdentityAsync関数を実行した時にAWSのAPIを叩く上で必要なトークンを得ることができます.このトークンを使って認可されるアクション,アクセスできるリソースはIAMで設定したCognito_HogeAuth_Roleに従います.

リフレッシュトークンはPlayerPrefなどに保存し,アプリを起動した際にIdトークンを更新することで自動ログイン処理を行います.

CognitoUserPoolClient.cs
namespace CognitoLogInSample
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using System;
    using System.Globalization;
    using Amazon;
    using Amazon.Runtime;
    using Amazon.CognitoIdentity;
    using Amazon.CognitoIdentity.Model;
    using Amazon.CognitoIdentityProvider;
    using Amazon.CognitoIdentityProvider.Model;

    public class CognitoUserPoolClient : MonoBehaviour
    {
        #region CognitoCredentials
        public string IdentityPoolId;            // IDプールのID
        public string CognitoIdentityRegion;     // リージョン名 例)ap-northeast-1

        private RegionEndpoint _CognitoIdentityRegion
        {
            get { return RegionEndpoint.GetBySystemName(CognitoIdentityRegion); }
        }
        private CognitoAWSCredentials _credentials;

        private CognitoAWSCredentials Credentials
        {
            get
            {
                if (_credentials == null)
                {
                    _credentials = new CognitoAWSCredentials(IdentityPoolId, _CognitoIdentityRegion);
                    _credentials.IdentityChangedEvent += Credentials_IdentityChangedEvent;
                }
                return _credentials;
            }
        }
        #endregion

        #region CognitoIdP
        public string CognitoIdPRegion;          // User Poolのリージョン 例)ap-northeast-1

        private RegionEndpoint _CognitoIdPRegion
        {
            get { return RegionEndpoint.GetBySystemName(CognitoIdPRegion); }
        }

        [SerializeField]
        string userPoolName;                     // User Poolの固有名 アンダーバーで区切ったうちの後半
        [SerializeField]
        string clientId;                         // User PoolのクライアントID

        string UserPoolId
        {
            get
            {
                return string.Format("{0}_{1}", _CognitoIdPRegion, userPoolName);
            }
        }

        public string CognitoIdentityProviderName
        {
            get
            {
                return string.Format("cognito-idp.{0}.amazonaws.com/{1}", _CognitoIdentityRegion.SystemName, UserPoolId);
            }
        }
        #endregion

        AmazonCognitoIdentityProviderClient idpClient;
        AmazonCognitoIdentityClient cognitoIdentityClient;

        public InputField IdInputField;
        public InputField passwordInputField;
        public Toggle clearCredentialToggle;

        TokenCacheManager tokenCacheManager;
        public bool cleanPlayerPrefSetting;

        string currentSession;
        string currentUserName;

        private void Start()
        {
            tokenCacheManager = new TokenCacheManager();
            if (cleanPlayerPrefSetting)
            {
                tokenCacheManager.DeleteCachedToken();
            }
            //AdminAuthenticateWithRefreshToken();
            SignInWithRefreshToken();
        }

        public void OnButtonClick()
        {
            if (clearCredentialToggle.isOn)
            {
                Credentials.Clear();
            }
            SignIn(IdInputField.text, passwordInputField.text);
        }

        #region InitiateAuthFlow

        /// <summary>
        /// Signs In with refresh token (USER_AUTH_FLOW).
        /// </summary>
        void SignInWithRefreshToken()
        {
            tokenCacheManager.GetCachedTokens((getCachedTokensResult) =>
            {
                if (getCachedTokensResult.IsCacheAvailable)
                {
                    Debug.Log(getCachedTokensResult.Token.ToString());
                    // RefreshToken
                    idpClient = new AmazonCognitoIdentityProviderClient(Credentials, _CognitoIdentityRegion);
                    InitiateAuthRequest initiateAuthRequest = new InitiateAuthRequest()
                    {
                        ClientId = clientId,
                        AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
                    };
                    initiateAuthRequest.AuthParameters.Add("REFRESH_TOKEN", getCachedTokensResult.Token.refreshToken);

                    idpClient.InitiateAuthAsync(initiateAuthRequest, (initiateAuthResponse) =>
                    {
                        if (initiateAuthResponse.Exception != null) return;
                        CognitoIdentityProviderToken cognitoIdentityProviderToken = new CognitoIdentityProviderToken
                        {
                            accessToken = initiateAuthResponse.Response.AuthenticationResult.AccessToken,
                            idToken = initiateAuthResponse.Response.AuthenticationResult.IdToken ?? getCachedTokensResult.Token.idToken,
                            refreshToken = initiateAuthResponse.Response.AuthenticationResult.RefreshToken ?? getCachedTokensResult.Token.refreshToken,
                            expireTime = initiateAuthResponse.Response.AuthenticationResult.ExpiresIn
                        };
                        tokenCacheManager.CacheTokens(cognitoIdentityProviderToken);

                        Credentials.AddLogin(CognitoIdentityProviderName, initiateAuthResponse.Response.AuthenticationResult.IdToken);
                        Credentials.GetIdentityIdAsync(responce =>
                        {
                            Debug.Log("Logged In with refreshed IdToken : " + responce.Response);
                        });
                    });
                    idpClient.Dispose();
                }
                else
                {
                    Credentials.Clear();
                    // Redirect to LogIn Dialog
                    Debug.Log("RefreshToken is not available");
                }
            });
        }


        /// <summary>
        /// Sign In.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <param name="password">Password.</param>
        void SignIn(string userName, string password)
        {
            Debug.Log("Initiate Authentication Flow");

            var cred = new AnonymousAWSCredentials();

            idpClient = new AmazonCognitoIdentityProviderClient(cred, _CognitoIdentityRegion);
            var TupleAa = AuthenticationHelper.CreateAaTuple();

            var initiateAuthRequest = new InitiateAuthRequest
            {
                AuthFlow = AuthFlowType.USER_SRP_AUTH,
                ClientId = clientId,
                AuthParameters = new Dictionary<string, string>(){
                            {"USERNAME", userName},
                            {"SRP_A", TupleAa.Item1.ToString(16)},
                        }
            };

            Debug.Log(initiateAuthRequest.AuthParameters["SRP_A"]);

            idpClient.InitiateAuthAsync(initiateAuthRequest, (initiateAuthResponse) =>
            {
                var challengeName = initiateAuthResponse.Response.ChallengeName;
                if (challengeName == ChallengeNameType.NEW_PASSWORD_REQUIRED)
                {
                    // newPasswordRequired
                    idpClient.Dispose();
                }
                else if (challengeName == ChallengeNameType.PASSWORD_VERIFIER)
                {
                    DateTime timestamp = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
                    var usCulture = new CultureInfo("en-US");
                    string timeStr = timestamp.ToString("ddd MMM d HH:mm:ss "UTC" yyyy", usCulture);

                    byte[] claim = AuthenticationHelper.authenticateUser(initiateAuthResponse.Response.ChallengeParameters["USERNAME"],
                                                                         password,
                                                                         userPoolName,
                                                                         TupleAa,
                                                                         initiateAuthResponse.Response.ChallengeParameters["SALT"],
                                                                         initiateAuthResponse.Response.ChallengeParameters["SRP_B"],
                                                                         initiateAuthResponse.Response.ChallengeParameters["SECRET_BLOCK"],
                                                                         timeStr
                                                                        );
                    string claimBase64 = Convert.ToBase64String(claim);

                    var respondToAuthChallengeRequest = new RespondToAuthChallengeRequest()
                    {
                        ChallengeName = initiateAuthResponse.Response.ChallengeName,
                        ClientId = clientId,
                        ChallengeResponses = new Dictionary<string, string>(){
                                    { "PASSWORD_CLAIM_SECRET_BLOCK", initiateAuthResponse.Response.ChallengeParameters["SECRET_BLOCK"]},
                                    { "PASSWORD_CLAIM_SIGNATURE", claimBase64 },
                                    { "USERNAME", userName },
                                    { "TIMESTAMP", timeStr }
                        }
                    };

                    Debug.Log(timeStr);

                    idpClient.RespondToAuthChallengeAsync(respondToAuthChallengeRequest, respondToAuthChallengeResponse =>
                   {
                       try
                       {
                           Debug.LogFormat("User was verified in SRP Auth Flow : {0}", respondToAuthChallengeResponse.Response.AuthenticationResult.IdToken);
                           Credentials.AddLogin(CognitoIdentityProviderName, respondToAuthChallengeResponse.Response.AuthenticationResult.IdToken);

                           tokenCacheManager.CacheTokens(new CognitoIdentityProviderToken()
                           {
                               accessToken = respondToAuthChallengeResponse.Response.AuthenticationResult.AccessToken,
                               idToken = respondToAuthChallengeResponse.Response.AuthenticationResult.IdToken,
                               refreshToken = respondToAuthChallengeResponse.Response.AuthenticationResult.RefreshToken,
                               expireTime = respondToAuthChallengeResponse.Response.AuthenticationResult.ExpiresIn,
                           });
                       }
                       catch (Exception e)
                       {
                           Debug.LogErrorFormat("Encountered exception: {0}", e);
                       }
                       finally
                       {
                           idpClient.Dispose();
                       }
                   });
                }
            });

            currentUserName = userName;
        }
        #endregion

        private void Credentials_IdentityChangedEvent(object sender, CognitoAWSCredentials.IdentityChangedArgs e)
        {
            Debug.Log(string.Format("Identity has changed from {0} to {1}", e.OldIdentityId, e.NewIdentityId));
        }
    }
}

参考資料

続きを読む