【AWS】Sign In with AppleでAWSサービスに接続する
はじめに
スマホのアプリ開発を行っていると、最近はクラウドとの連携が必要になる場合がほとんどです。
その際、ログインアカウントをどうするか、という問題に突き当たります。
私が公開しているLiveCapture3 Remoteも、内部的にAWSに接続して処理を行いますが、そこで利用したのが、Googleアカウント/Apple IDを利用してアプリサインインを行う方法です。
この方法を使用すれば、ユーザアカウント情報を自サービスで管理せずに、アプリに対して適切な権限を付与して必要最低限のAWSサービスにアクセスすることが可能になります。
今回は、AWS Cognitoサービスを使用してSign In with Appleでサインインし、ユーザに対して適切なIAM権限を付与する方法を説明します。
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