モバイルアプリでユーザー認証やデータ同期が行えるAmazon CognitoがiOSで動かないのを何とか調べた話
モバイルアプリにユーザー認証とデータ同期を組み込むAmazon Cognitoを試してみたのですが、 ネット上のサンプルコードが古く、えらく苦労したので上手く動いた方法を掲載します。
Amazonの公式ドキュメントも情報が古いままで、今回掲載した方法が正しいとは限らないので、 これから実装される方は注意してください。 そのうち公式のドキュメントやサンプルも更新されると思います。
Amazon Cognitoについて
https://aws.amazon.com/jp/cognito/
AmazonのWebサービスとして提供されているユーザー認証とデータ同期を行ってくれるサービスです。
- OAuthを使ったユーザー認証
- 独自のユーザー認証
- 端末間データの同期
- オフライン対応
上記が主な機能です。
特に端末間のデータ同期については独自に実装するとかなり煩わしいので、 その辺りの面倒をみてもらえるのはありがたいです。
データはKye-Value型のデータストアで、内部的にはSQLiteで管理されているようでした。(iOSの場合)
ちなみにAmazon Mobile Hubというサービスの中にもAmazon Cognitoが組み込まれていますが、 Twitterや独自ユーザーの認証がない等、機能の違いが見られたため、今回はCognitoを使っています。
前提条件
- XCode ver7.3
- Swift ver2.2
- AWSCognito ver2.4.1
- AWSCognitoIdentityProvider ver2.4.1
- Facebook-iOS-SDK ver4.1.0
今回はFacebookのログインだけを試しました。 複数端末でログインを行い、データが同期されるかを試します。
AWSCognitoがどのバージョンから使い方が変わっているのかは調べていないですが、 少なくともver2.2以降のどこかで変わっているようです。
ユーザー認証までのプロセス
前準備
上記が必要になります。 少し古いですが、下記が参考になると思います。
Facebookアプリの認証は公式が参考になります。
インストール
必要なライブラリはCocoapodsでインストールができます。
pod 'AWSCognito' pod 'AWSCognitoIdentityProvider' pod "Facebook-iOS-SDK"
オブジェクトの生成
古い書き方
古いSDKだと下記のような感じです。
ログインに必要な情報をloginsプロパティに渡す形ですが、このコードは新しいSDKでは動きません。
let credentialsProvider = AWSCognitoCredentialsProvider.credentialsWithRegionType( AWSRegionType.USEast1, accountId: cognitoAccountId, identityPoolId: cognitoIdentityPoolId, unauthRoleArn: cognitoUnauthRoleArn, authRoleArn: cognitoAuthRoleArn ) let token = FBSession.activeSession().accessTokenData.accessToken var logins = [AWSIdentityProviderFacebook : token] credentialsProvider.logins = logins let defaultServiceConfiguration = AWSServiceConfiguration( region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider ) AWSServiceManager.defaultServiceManager().setDefaultServiceConfiguration(defaultServiceConfiguration)
新しい書き方
SDKの新バージョンではAWSIdentityProviderManagerプロトコルを実装したクラスをイニシャライザに渡す必要があります。
AWSIdentityProviderManagerプロトコルはlogins()の実装が必須になっており、 ログインに必要な情報を追加したAWSTaskオブジェクトを返却する必要があります。
下記の例ではFacebookが認証済みであれば、facebookのトークン情報を渡すようにしています。
ちなみにAWSTaskクラスは同期処理、非同期処理などをプロミスっぽい感じで書けるようにするクラスです。 AWSのSDKでは至ることで使われているようです。
class MyProvider:NSObject, AWSIdentityProviderManager{ func logins() -> AWSTask { var providers = [String:String]() if let fbtoken = FBSDKAccessToken.currentAccessToken(){ providers[AWSIdentityProviderFacebook] = fbtoken.tokenString } return AWSTask(result: providers as AnyObject) } }
AWSIdentityProviderManagerを実装したクラスを作成したら、そのオブジェクトをAWSCognitoCredentialsProviderに渡してあげます。 あとはAWSCognitoCredentialsProviderのcredentialsメソッドを呼び出してあげることで、ユーザーの認証が行われます。
credentials()を呼び出した時にAWSIdentityProviderManagerのloginsが呼ばれます。
コードにすると下記のような形です。
let myProvider = MyProvider() let credentialsProvider = AWSCognitoCredentialsProvider( regionType: .APNortheast1, identityPoolId: Define.identityPoolId, unauthRoleArn: Define.unauthRoleArn, authRoleArn: Define.authRoleArn, identityProviderManager: myProvider ) let configuration = AWSServiceConfiguration(region:.APNortheast1, credentialsProvider:credentialsProvider) AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration credentialsProvider.credentials().continueWithSuccessBlock { (task: AWSTask) -> AnyObject? in return nil }
ユーザーIDの取得
ユーザーIDの取得自体は新SDKでも同じで下記のような感じになります。
credentialsProvider.getIdentityId().continueWithBlock { (task: AWSTask) -> AnyObject? in let cognitoId = self.credentialsProvider.identityId print(cognitoId!) return nil }
ただ、1点注意があって、新SDKでは先にユーザー認証を行なっていないと、 ログイン情報があるにも関わらず、匿名ユーザーとして扱われてしまいます。
そのため、認証を行ったあとにAWSTaskを利用して認証後にgetIdentityId()を呼び出すようにします。
credentialsProvider.credentials().continueWithSuccessBlock { (task: AWSTask) -> AnyObject? in return nil }.continueWithSuccessBlock { (task:AWSTask) -> AnyObject? in return self.credentialsProvider.getIdentityId().continueWithBlock { (task: AWSTask) -> AnyObject? in let cognitoId = self.credentialsProvider.identityId print.setId(cognitoId!) return nil } }
データセットの扱いについて
データセットについては新SDKでも特に変わらないようでした。
// 同期クライアントの生成 let syncClient = AWSCognito.defaultCognito() // データセットの生成 let dataset = syncClient.openOrCreateDataset("myDataset") // データの取得 if let value = dataset.stringForKey("myData"){ print("myData: \(myData)") } // 削除(この時点ではローカルデータのみ) dataset.clear() // 保存(この時点ではローカルデータのみ) dataset.setString("myValue", forKey:"myData") // データの同期 dataset.synchronize().continueWithBlock { (task: AWSTask) -> AnyObject? in if task.cancelled{ print("同期キャンセル") }else if let error = task.error{ print("同期エラー",error) }else{ print("同期完了") } return nil }
基本的には上記のような感じですが、新SDKではユーザーの認証後に行なう必要があるため、 下記のような感じでAWSTaskを繋げます。
func getDataset() -> AWSCognitoDataset{ let syncClient = AWSCognito.defaultCognito() let dataset = syncClient.openOrCreateDataset("myDataset") return dataset } credentialsProvider.credentials().continueWithSuccessBlock { (task: AWSTask) -> AnyObject? in return nil }.continueWithSuccessBlock { (task: AWSTask) -> AnyObject? in let dataset = self.getDataset() // データの同期 return dataset.synchronize().continueWithBlock { (task: AWSTask) -> AnyObject? in return nil } }.continueWithSuccessBlock { (task:AWSTask) -> AnyObject? in let dataset = self.getDataset() // この辺で必要な処理を行なう if let value = dataset.stringForKey("myData"){ print("myData: \(myData)") } }
認証を最初に行い、次にデータの同期、その後に必要な処理を行なう感じです。
let token = FBSession.activeSession.accessTokenData.accessToken credentialsProvider.logins = [AWSIdentityProviderFacebook : token ];
Facebookの認証
やり方は色々あるようですが、FacebookSDKのログインボタンを使うのが一番簡単です。
1.AppDelegateに処理を追加
下記の2つのDelegateを追加します。
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Override point for customization after application launch. FBSDKApplicationDelegate.sharedInstance().application(application, didFinishLaunchingWithOptions: launchOptions) return true } func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool { return FBSDKApplicationDelegate.sharedInstance().application(application, openURL: url, sourceApplication: sourceApplication, annotation: annotation) }
2.ログインボタンの配置
StoryBoardで設置します。 FBSDKLoginButtonはUIButtonを継承しているので、 下記のようにUIButtonを設置してからクラスを指定してあげます。
3.FBSDKLoginButtonDelegateを追加
基本的には1と2を追加した段階で、Facebookの認証はできるようになります。 ボタンを押したあとの処理はFacebookのSDKが丸々引き受けてくれます。
ログインやログアウトのタイミングで処理を行いたい時は、delegateを追加します。 下記ではviewWithTagでボタンを取得していますが、outletでも問題ないと思います。
class ViewController: UIViewController, FBSDKLoginButtonDelegate{ override func viewDidLoad() { super.viewDidLoad() let fbLoginButton: FBSDKLoginButton = view.viewWithTag(Tag.FBLogin.hashValue) as! FBSDKLoginButton fbLoginButton.delegate = self } func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!) { // Your Code } func loginButtonDidLogOut(loginButton: FBSDKLoginButton!) { // Your Code } }
3.認証トークンの取得
Cognitoに認証情報を渡す時は下記のような形で取得ができます。
let token = FBSDKAccessToken.currentAccessToken()
まとめ
2016年5月4日時点では公式や英語圏も含めて参考になる情報がなかったので、 iOSのAmazon Cognitoが動かなくて困っている方は参考にしてみてください。