アニマネ開発日誌

アニメアプリのアニマネの開発日誌です。

モバイルアプリでユーザー認証やデータ同期が行えるAmazon CognitoがiOSで動かないのを何とか調べた話

モバイルアプリにユーザー認証とデータ同期を組み込むAmazon Cognitoを試してみたのですが、 ネット上のサンプルコードが古く、えらく苦労したので上手く動いた方法を掲載します。

Amazonの公式ドキュメントも情報が古いままで、今回掲載した方法が正しいとは限らないので、 これから実装される方は注意してください。 そのうち公式のドキュメントやサンプルも更新されると思います。

Amazon Cognitoについて

https://aws.amazon.com/jp/cognito/

AmazonWebサービスとして提供されているユーザー認証とデータ同期を行ってくれるサービスです。

  • OAuthを使ったユーザー認証
  • 独自のユーザー認証
  • 端末間データの同期
  • オフライン対応

上記が主な機能です。

特に端末間のデータ同期については独自に実装するとかなり煩わしいので、 その辺りの面倒をみてもらえるのはありがたいです。

データはKye-Value型のデータストアで、内部的にはSQLiteで管理されているようでした。(iOSの場合)

ちなみにAmazon Mobile Hubというサービスの中にもAmazon Cognitoが組み込まれていますが、 Twitterや独自ユーザーの認証がない等、機能の違いが見られたため、今回はCognitoを使っています。

前提条件

今回はFacebookのログインだけを試しました。 複数端末でログインを行い、データが同期されるかを試します。

AWSCognitoがどのバージョンから使い方が変わっているのかは調べていないですが、 少なくともver2.2以降のどこかで変わっているようです。

ユーザー認証までのプロセス

  1. facebook等の外部プロバイダーで認証を行う。
  2. 外部プロバイダーから割り当てられたトークンをAWSに渡して認証を行う。
  3. AWSからユーザーの認証情報を取得する。

前準備

  1. Amazon CognitoでIdentity Poolの作成
  2. Facebookアプリの作成
  3. Facebookアプリで認証を利用する為の設定

上記が必要になります。 少し古いですが、下記が参考になると思います。

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クラスは同期処理、非同期処理などをプロミスっぽい感じで書けるようにするクラスです。 AWSSDKでは至ることで使われているようです。

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を設置してからクラスを指定してあげます。

f:id:animane:20160504195837p:plain

3.FBSDKLoginButtonDelegateを追加

基本的には1と2を追加した段階で、Facebookの認証はできるようになります。 ボタンを押したあとの処理はFacebookSDKが丸々引き受けてくれます。

ログインやログアウトのタイミングで処理を行いたい時は、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()

まとめ

  • Amazon Cognitoの新SDKでは認証方法が変わっているので気を付けること。
  • AWSTaskは便利なのでガンガン使っていけそう。
  • Facebookの認証はSDKのボタンで行なうのが簡単。

2016年5月4日時点では公式や英語圏も含めて参考になる情報がなかったので、 iOSAmazon Cognitoが動かなくて困っている方は参考にしてみてください。