📚

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

2023/04/16に公開

はじめに

スマホのアプリ開発を行っていると、最近はクラウドとの連携が必要になる場合がほとんどです。
その際、ログインアカウントをどうするか、という問題に突き当たります。

私が公開しているLiveCapture3 Remoteも、内部的にAWSに接続して処理を行いますが、そこで利用したのが、Googleアカウント/Apple IDを利用してアプリサインインを行う方法です。

この方法を使用すれば、ユーザアカウント情報を自サービスで管理せずに、アプリに対して適切な権限を付与して必要最低限のAWSサービスにアクセスすることが可能になります。

今回は、AWS Cognitoサービスを使用してSign In with Appleでサインインし、ユーザに対して適切なIAM権限を付与する方法を説明します。

googleでのサインイン方法はこちらをご覧ください

Sign In with Appleの有効化

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

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

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

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

<?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プロバイダー」を選択して、「プロバイダの作成」ボタンをクリックします。

項目 入力値
プロバイダーのタイプ 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」が選択肢として表示されますので、チェックを入れます。

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

AWS認証情報の権限設定

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

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

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

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

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

実装

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

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

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

[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の実装詳細に関しては別の文献を参照してください。
(名前とメールアドレスは最初の1回目しか取得できないとか、色々と癖があります。。。)

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

  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が発生します。

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の方法があれば、どなたか教えてください。。

#参考

Face IDでログイン!Sign in with AppleをiOSアプリに組み込む
signin with apple 実装するときの注意点まとめ

Discussion