UnityでAWS SDK for iOSを使ってCognito UserPoolsにサインアップする

2018.04.27

問題の概要

AWS SDK for .NETを使うUnityアプリをiOS/Android向けにビルドするの記事で残った問題として、AWS SDK for .NETをUnityで使うとTlsExceptionが発生するというものがありました。

Unity上で動作するC#/.NET FrameworkのHTTPクライアントのうち、HttpWebRequestはSHA256の証明書を使用できないとの報告があります。 AWS SDKに限らず for .NET と位置付けられている、HttpWebRequestを使うSDKは、Unity実行時に同様の問題を抱えているのだと思います。AWS SDK for .NETのコードを全て読んだわけではありませんが、コードを検索する限り、HttpWebRequestとUnityWebRequestが混在しているようです。 執筆時点でAWS SDK for UnityはAWS SDK for .NETと同じリポジトリに含まれており、AWS SDK for Unity側ではTlsExceptionが出ないUnityWebRequestを使っているようです。したがって、これがサポートしていないAPIでは部分的にTlsExceptionが起きるものと思われます。
タイトルのCognito UserPoolsの場合、サインインはCognitoAuthentication Extension Libraryで問題なく動く実装が行われているのですが、サインアップは実装されていないません。UserPoolsはAWS SDK for Unityにも実装されていないため、AWS SDK for .NETを使うことになります。AWS SDK for .NETではHttpWebRequestが使われているため、Unityで動かした場合にTlsExceptionが発生してしまします。

PCで実行する際にも、OSが古いといった特定条件下で発生します。これは、monoもAWS SDK for .NETも証明書を同梱しておらず、参照先のOSの証明書が古いために発生します。その場合は冒頭の記事でお伝えした通り、mozrootsというツールで最新の証明書を取得することで解決します。しかしながらこの方法はモバイルアプリ向けにビルドした環境では使えません。また、OSからの証明書取得もうまくいかないようです。同様の調査をおこなっている記事を見つけました。技術的な詳細はこちらが詳しいので参照してください。
Unityにおける通信APIを色々試して罠を踏んだ話

結果として、iOS/AndroidをターゲットにしたUnityアプリでHttpWebRequestはAWSにHTTPS通信できない、ということになります。この問題に対処するため、今回はUnityのNative Plugin機能を使い、AWSへのリクエストをAWS SDK for iOSで行うことにしました。

Unity Native Pluginとは

UnityのC#を変換したコードではなく、iOSならObjective-C、AndroidならJARをUnityから実行する方法です。

今回はiOSのみ記載しますが、同様の方法でAWS SDK for Androidを使うことで、TlsExceptionの問題に対処することができると思います。

実装

Apple Developerで必要な値を取得してUnityのビルド設定に追加

UnityがiOSアプリをビルドする時は、まずUnityが.xcodeprojを生成して、それをXCodeでビルドする流れになります。従って、XCodeでビルドするために必要な設定はUnity側で行い、それに沿って自動生成するようにします。
まずは最低限必要な設定をします。以下の画像のように、Bundle Identifierを作成し、Team IDと共に確認します。

Certificates, IDs & Profilesをクリック

iOS App IDの新規作成フォームに移動

適切な値を入力して作成

一覧画面からApp IDを検索し、作成されていることを確認

Unity iOS Player Settingsで設定

Unityエディタ拡張の設定

先ほどのビルド設定に加えて、以下の作業も行います。

Unity EditorでObjective-Cファイルなどを作成できるようにする

Unityは各ファイルに対応する.metaファイルを自動生成し、管理しています。この処理の詳細は公式ドキュメントを参照してください。
ここで重要なのは、Unity Editor上でファイルを作成する必要があるということです。コマンドラインのような別の方法で作成すると、.metaファイルが作成されず、Unity Editorから認識されません。しかし、デフォルトではUnity EditorでObjective-Cファイルを作成することができません。ここでは、Unity Editorを拡張することでObjective-Cファイルを作成する方法を説明します。詳細はこちらをご覧ください。

以下のコマンドはMacの場合です。

$ cd /Applications/Unity/Unity.app/Contents/Resources/ScriptTemplates
$ touch 82-Custom__ObjectiveC++-Plugin.mm.txt

この他にもいくつかのテンプレートを追加した後のUnity Editorがこちらになります。

後の手順で、ここから.mmファイルを作成します。

.xcodeproj生成時にEmbedded Frameworksが設定されるようにする

Unityが生成した.xcodeprojに手を加えずにXCodeでビルドできると、開発やCI/CDが楽になります。後述するAWS SDKはEmbedded Frameworkとして追加する必要があり、デフォルトの状態であればUnityでxcodeprojを生成した後、XCodeを操作し手動でEmbedded Frameworkに追加する必要があります。これを自動化することで、UnityからiOS実機確認がスムーズに行える状態を維持できます。

これを実現するために、UnityのPBXProjectを使います。こちらを参考にして、UnityのAssets/Editor/ディレクトリ直下に以下のC#コードを追加します。

using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.iOS.Xcode;
using UnityEditor.iOS.Xcode.Extensions;
using UnityEditor.Callbacks;
using System.Text.RegularExpressions;

public class XcodeSettingsPostProcesser {
    [PostProcessBuildAttribute(1)]
    public static void OnPostprocessBuild(BuildTarget target, string projectPath) {

        if (target != BuildTarget.iOS) return;

        string pbxProjPath = PBXProject.GetPBXProjectPath(projectPath);
        PBXProject pbxProject = new PBXProject();
        pbxProject.ReadFromFile(pbxProjPath);

        string targetName = PBXProject.GetUnityTargetName();
        string targetGuidName = pbxProject.TargetGuidByName(targetName);
        pbxProject.WriteToFile(pbxProjPath);

        string basePath = Application.dataPath + "/";
        string frameworkPath = "Plugins/iOS/";
        string []arrFrameworks = {"AWSCore.framework", "AWSCognitoIdentityProvider.framework", "AWSCognitoIdentityProviderASF.framework"};
        foreach(string framework in arrFrameworks) {
            AddEmbeddedFramework(ref pbxProject, targetGuidName, basePath + frameworkPath + framework, frameworkPath + framework);
        }

        pbxProject.WriteToFile (pbxProjPath);
        foreach(string framework in arrFrameworks) {
            string contents = File.ReadAllText(pbxProjPath);
            string pattern = "(?<=Embed Frameworks)(?:.*)(\\/\\* " + framework + "\\ \\*\\/)(?=; };)";
            string oldText = "/* " + framework + " */";
            string updatedText = "/* " + framework + " */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }";
            contents = Regex.Replace(contents, pattern, m => m.Value.Replace(oldText, updatedText));
            File.WriteAllText(pbxProjPath, contents);
        }
    }

    public static void AddEmbeddedFramework(ref PBXProject project, string target, string frameworkPath, string frameworkName) {
        string fileGuid = project.AddFile (frameworkPath, "Frameworks/" + frameworkName, PBXSourceTree.Source);
        string embedPhase = project.AddCopyFilesBuildPhase (target, "Embed Frameworks", "", "10");
        project.AddFileToBuildSection (target, embedPhase, fileGuid);
        PBXProjectExtensions.AddFileToEmbedFrameworks (project, target, fileGuid);
        project.AddBuildProperty (target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");
        project.AddBuildProperty (target, "FRAMEWORK_SEARCH_PATHS", "$(SRCROOT)/PATH_TO_FRAMEWORK/");
    }
}

Binding部分の作成

AWS SDK for iOSの追加

AWS SDK for iOSをダウンロード・展開した後、これから必要になる以下ファイルをUnityのAssets/Plugins/iOS/にコピーします。

  • AWSCore.framework
  • AWSCognitoIdentityProvider.framework
  • AWSCognitoIdentityProviderASF.framework

C#側のコードを作成

ネイティブプラグインを呼び出すコードを作成します。

using UnityEngine;
using System.Runtime.InteropServices;

public class Binding {
    [DllImport("__Internal")]
    private static extern void signup_(string email, string password);

    public static void signup(string email, string password) {
        if (Application.platform == RuntimePlatform.IPhonePlayer) {
            signup_ (email, password);
        }
    }
}

Objective-C側のコードを作成

以下のようなコードをUnityのAssets/Plugins/iOS/に作成します。ここから先はAWS SDK for iOSのCognito UserPoolsドキュメントを参照してください。

#import <Foundation/Foundation.h>
#import <AWSCognitoIdentityProvider/AWSCognitoIdentityProvider.h>

extern "C" {
    void signup_(const char* argEmail, const char* argPassword) {
        NSLog(@"signup called");

        NSString *email = [NSString stringWithCString:argEmail encoding:NSUTF8StringEncoding];
        NSString *password = [NSString stringWithCString:argPassword encoding:NSUTF8StringEncoding];

        // setup service config
        AWSServiceConfiguration *serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSWest2 credentialsProvider:nil];
        // create a pool
        AWSCognitoIdentityUserPoolConfiguration *configuration = [[AWSCognitoIdentityUserPoolConfiguration alloc] initWithClientId:@"MY_CLIENT_ID"
                                                                                                                      clientSecret:@"MY_CLIENT_SECRET"
                                                                                                                            poolId:@"MY_POOL_ID"];
        [AWSCognitoIdentityUserPool registerCognitoIdentityUserPoolWithConfiguration:serviceConfiguration userPoolConfiguration:configuration forKey:@"UserPool"];
        AWSCognitoIdentityUserPool *pool = [AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:@"UserPool"];

        // sign up
        AWSCognitoIdentityUserAttributeType * emailAttributeType = [AWSCognitoIdentityUserAttributeType new];
        emailAttributeType.name = @"email";
        emailAttributeType.value = email;
        [[pool signUp:email password:password userAttributes:@[emailAttributeType] validationData:nil] continueWithBlock:^id _Nullable(AWSTask<AWSCognitoIdentityUserPoolSignUpResponse *> * _Nonnull task) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if(task.error){
                    NSLog(@"%@", task.error);
                }else {
                    AWSCognitoIdentityUserPoolSignUpResponse * response = task.result;
                    if(!response.userConfirmed){
                        //need to confirm user using user.confirmUser:
                    }
                    NSLog(@"%@", response);
                }});
            return nil;
        }];
    }
}

動作確認

実行すると、XCodeにログが出力されます。

まとめ

UnityでiOS/Androidをターゲットにした際にAWS SDK for .NETが利用できなかったので、Unity Native Pluginを使って実現しました。
CognitoAuthentication Extension Libraryの今後の動向によっては、このようなworkaroundが不要になるかもしれません。あるいは、UnityがバージョンアップしてHttpWebRequestの問題が解決するかもしれません。
開発元であるAWSやUnity Technologiesにフィードバックしていきましょう。