Sign In with AppleでAWSサービスに接続する

# はじめに
こんにちは。Daddy's Officeの市川です。

先日、「LiveCapture3 Remote」のiOS版をリリースしました。
今回は開発にXamarin.Formsを使用して、Android/iOS両対応の開発を行いました。

「LiveCapture3 Remote」では、クラウドサーバにAWSを使用したアプリケーションですが、Android版のアプリサインインは、Googleアカウントでサインインし、AWS Cognitoでユーザに対して適切なIAM権限を付与する形で実現しています。

Googleアカウントでのサインインに関してはこちらを参照してください。
[GoogleアカウントでAWSサービスに接続する][1]

今回iOS版では、必須になっているSign In with Appleでサインインし、Android版と同様に、AWS Cognitoで適切なIAM権限をアプリに付与してAWSの各種サービスに直接アクセスできるようにしました。

今回は、このやり方を説明しようと思います。

ちなみに「LiveCapture3 Remote」はiOS13以降のみ対応なので、iOS12以前の説明は行いません。（というか分かりません。。。）

# Sign In with Appleの有効化

まず、Apple Developerで、Sign In with Appleを有効にします。

Apple Developerを開き、（アプリのApp IDを登録していない場合は登録して）、「Certificates, Identifiers & Profiles」で対象アプリを選択します。

![AppDeveloper01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382663/d094c54e-682f-35aa-52ef-cf0bf8c7ec27.jpeg)

設定の「Capabilities」をスクロールすると「Sign In with Apple」がありますので、チェックを付けます。

![AppDeveloper02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382663/d51906cc-2aa6-9759-c18a-12cbaeb023b1.jpeg)

あとは、プロジェクトのEntitlements.plistに「com.apple.developer.applesignin」のキーを下記のように追加します。

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.developer.applesignin</key>
	<array>
		<string>Default</string>
	</array>
</dict>
</plist>
```
# IDプロバイダーの追加(AWS)
Apple IDアカウントで認証済みのアプリからのアクセスに対して、AWSサービスへアクセスするAWS認証情報を発行するために、CognitoのIDプールとApple IDアカウントを連携させます。

IAMの「IDプロバイダー」を選択して、「プロバイダの作成」ボタンをクリックします。


![IAM01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382663/199c8267-f636-e787-44c2-1b65f6a2b321.jpeg)

|項目|入力値|
|:-----------------|:-------------|
|プロバイダーのタイプ|OpenID Connect|
|プロバイダーのURL|https:///appleid.apple.com|
|対象者|App Developerで設定したアプリのIdentifier(com.xxxxx.xxxxx)|


上記を入力して、IDプロバイダー(appleid.apple.com)を作成します。

# Cognito IDプールの作成(AWS)

OpenIDのIDプロバイダを追加すると、CognitoのIDプール作成画面のOpenIDのタブに、今追加した「appleid.apple.com」が選択肢として表示されますので、チェックを入れます。

![Cognito01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382663/f904f854-08cb-8219-fc10-dadf693485cc.jpeg)

今回は認証されていないIDのアクセスは拒否しますので、そのままIDプールを作成します。

# AWS認証情報の権限設定

CognitoでIDプールを作成すると、２つのIAMロールが生成されます。

+ Cognito_xxxxxxAuth_Role (認証済みユーザ用のIAMロール）
+ Cognito_xxxxxxUnauth_Role（認証されていないユーザ用のIAMロール）

(xxxxxxは作成したIDプールの名称です)

先ほどのIDプール作成で、「認証されていないIDに対してのアクセスを有効にする」にチェックを入れると、認証していないユーザにもAWSサービスへのアクセスを許可することができます。

ただ、今回は使用しませんので、認証済みユーザ向けの「Cognito_xxxxxxxAuth_Role」の方に、必要なアクセス権を設定します。


#実装

以下を参考に実装します。
[Xamarin.iOS Appleでのサインイン][2]

今回はXamarin.Formsを使用して開発していますので、Sign In with Appleの処理はDependency Serviceとして実装します。
（Android側はGoogle SignInの処理を記述しました）

iOS側のDependency Serviceは以下のような感じです。

```C#
[assembly: Xamarin.Forms.Dependency(typeof(AppleSignInService))]
namespace lc3remote.iOS.Services
{
    public class AppleSignInService: NSObject, ISignInService,IASAuthorizationControllerDelegate, IASAuthorizationControllerPresentationContextProviding
    {
        AuthManager authManager;

        public async Task<SignInUser> SignInAsync()
        {
            var appleIdProvider = new ASAuthorizationAppleIdProvider();
            var request = appleIdProvider.CreateRequest();
            request.RequestedScopes = new[] { ASAuthorizationScope.Email, ASAuthorizationScope.FullName };

            authManager = new AuthManager(UIApplication.SharedApplication.KeyWindow);

            var authorizationController = new ASAuthorizationController(new[] { request });
            authorizationController.Delegate = authManager;
            authorizationController.PresentationContextProvider = authManager;
            authorizationController.PerformRequests();

            var creds = await authManager.Credentials;

            if (creds == null)
                return null;

            var user = new SignInUser(); 	// 共通ユーザクラス
            user.Id = creds.User;			// ユーザID
            user.jwt = new NSString(creds.IdentityToken, NSStringEncoding.UTF8).ToString(); // JWT Token

            return user;

        }

        //
        // awaitできるようにSignIn処理をクラス化する
        //
        class AuthManager : NSObject, IASAuthorizationControllerDelegate, IASAuthorizationControllerPresentationContextProviding
        {
            public Task<ASAuthorizationAppleIdCredential> Credentials
                => tcsCredential?.Task;

            TaskCompletionSource<ASAuthorizationAppleIdCredential> tcsCredential;

            UIWindow presentingAnchor;

            public AuthManager(UIWindow presentingWindow)
            {
                tcsCredential = new TaskCompletionSource<ASAuthorizationAppleIdCredential>();
                presentingAnchor = presentingWindow;
            }

            public UIWindow GetPresentationAnchor(ASAuthorizationController controller)
                => presentingAnchor;

            [Export("authorizationController:didCompleteWithAuthorization:")]
            public void DidComplete(ASAuthorizationController controller, ASAuthorization authorization)
            {
                var creds = authorization.GetCredential<ASAuthorizationAppleIdCredential>();
                tcsCredential?.TrySetResult(creds);
            }

            [Export("authorizationController:didCompleteWithError:")]
            public void DidComplete(ASAuthorizationController controller, NSError error)
                => tcsCredential?.TrySetException(new Exception(error.LocalizedDescription));
        }
    }
}

```
Sign In with Appleの実装詳細に関しては別の文献を参照してください。
（名前とメールアドレスは最初の１回目しか取得できないとか、色々と癖があります。。。）

サインインが成功するとASAuthorizationAppleIdCredentialが取得できます。
このクラスのIdentityTokenメンバーがOpenIDで使用するJWTトークンになりますので、それを使用して、AWSサービスにアクセスする際のCredentialオブジェクトを生成します。

```C#
　　var credentials = new CognitoAWSCredentials(COGNITO_IDP_ID, RegionEndpoint.APNortheast1);
　　credentials.AddLogin("appleid.apple.com", user.IdToken);
```
このCredentialをAWS SDKの各サービス用クライアント生成時に使用することで、AWSサービスに設定された権限でアクセスが可能になります。

#注意事項
取得した認証トークンには有効期限があります。
Sign In with Appleで取得できるトークンの期限は10分で、この期限を超えて取得できたトークンを使用するとAWS SDKから、下記のExceptionが発生します。

```C#
Amazon.CognitoIdentity.Model.NotAuthorizedException
```
これをCatchしたら、再ログイン処理を行ってから再試行する、という実装が必要です。

この再ログイン処理ですが、Googole SignInでは、ユーザに再度サインイン画面を表示しなくても、内部でRefreshTokenによるAccess Tokenの再発行が行えます。（Silent SignIn)

しかし、Sign In with Appleでは、Refresh TokenによるAccess Tokenの再発行処理の口がありません。。。
（私が見つけられていないだけかも。。。）

今回私が開発した「LiveCapture3 Remote」では、10分後に再度サインイン画面を表示する、という流れでも特に問題なかったので、そのままにしていますが、もし、Sign In with AppleでのSilent SignInの方法があれば、どなたか教えてください。。

#参考

[GoogleアカウントでAWSサービスに接続する][1]
[Face IDでログイン！Sign in with AppleをiOSアプリに組み込む][3]
[signin with apple 実装するときの注意点まとめ][4]


[1]:https://qiita.com/Yukio-Ichikawa/items/b8a30181470283847203
[2]:https://docs.microsoft.com/ja-jp/xamarin/ios/platform/ios13/sign-in
[3]:https://dev.classmethod.jp/articles/sign-in-with-apple-implementation/
[4]:https://qiita.com/huiping192/items/c9b94d30a021885cb2df