Amazon Cognito Identity でFacebookやTwitterログインの名寄せをしてみた


[AWS SDK for Ruby V2](http://docs.aws.amazon.com/sdkforruby/api/)のCognitoIdentity::Clientクラスを使ってみました。

## サンプルコード

```rb
logins = {
    'graph.facebook.com' => 'facebook_oauth2_access_token',
    'api.twitter.com' => ['twitter_user_token', 'twitter_user_secret'].join(';')
}

cognito = Aws::CognitoIdentity::Client.new(region: 'us-east-1')
resp = cognito.get_id(
    identity_pool_id: 'us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX',
    logins: logins
)
pp resp.identity_id # => "us-east-1:YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
```

## 説明

まず、Cognitoを使うとき、FacebookやTwitterのアクセストークンの取得操作は、自前で行う必要があります。（Amazon Cognito側ではない）

利用する際は事前に取得したアクセストークンをCognitoに渡すことで、Cognitoの側でユーザーID等を取得して、Cognito IDを割り当ててくれます。

その際に、複数の認証プロバイダのトークンをハッシュで渡すことで、それらを1つのCognito IDに割り当てる処理を行ってくれます。

アプリケーション側ではこのCognito IDを使ってユーザーを識別することで、簡単に名寄せを実現できます。

### プロバイダトークンのハッシュを準備

```rb
logins = {
    'graph.facebook.com' => 'facebook_oauth2_access_token',
    'api.twitter.com' => ['twitter_user_token', 'twitter_user_secret'].join(';')
}
```

`'プロバイダ' => 'トークン'`のハッシュを作ります。[ドキュメント](http://docs.aws.amazon.com/cognito/devguide/identity/external-providers/twitter/)に記載がありますが、TwitterについてはAPIから得られる`token`と`secret`を`;`でつないだ文字列を使う必要がある点に注意して下さい。

### Aws::CognitoIdentity::Clientインスタンスを作る

http://docs.aws.amazon.com/sdkforruby/api/Aws/CognitoIdentity/Client.html#initialize-instance_method

```rb 
cognito = Aws::CognitoIdentity::Client.new(region: 'us-east-1')
```

### Aws::CognitoIdentity::Client#get_idでCognito IDを得る

http://docs.aws.amazon.com/sdkforruby/api/Aws/CognitoIdentity/Client.html#get_id-instance_method

```rb 
resp = cognito.get_id(
    identity_pool_id: 'us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX',
    logins: logins
)
pp resp.identity_id # => "us-east-1:YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
```

事前に作成しておいたIdentity PoolのID（REGION:GUID形式）と、先ほど作成したプロバイダトークンのハッシュを渡します。

応答の`#identity_id`でREGION:GUID形式のCognitoIDを得られました。

なお、このAPIは公開APIなので、AWSのアクセストークンは不要とのこと。

## Rails + Omniauthで実験したコード

以下、実際に実験したコードの一部を張っておきます。

```rb 
class AuthController < ApplicationController
  # GET /auth/:provider/callback
  def callback
    # current_user.logins
    # ログイン中ならユーザーの（有効期限内の）アクセストークンのハッシュを得る、
    # 未ログインなら空の配列を用意する
    logins = current_user.present? ? current_user.logins : {}
    # 今回ログインしたプロバイダのトークンを準備する
    case omniauth[:provider]
      when 'facebook'
        logins['graph.facebook.com']  = omniauth[:credentials][:token]
      when 'twitter'
        logins['api.twitter.com']  = [omniauth[:credentials][:token], omniauth[:credentials][:secret]].join(';')
      else
        raise 'Unknown Provider'
    end

    # Cognito IDの取得し、それをユーザーの識別に使う
    cognito = Aws::CognitoIdentity::Client.new(region: 'us-east-1')
    resp = cognito.get_id(
        identity_pool_id: Rails.application.secrets.aws_cognito_identity_pool_id,
        logins: logins,
    )
    identity_id= resp.identity_id
    Rails.logger.debug "Cognito IdentityID: #{identity_id}"
    user = User.where(identity: identity_id).first_or_create

    # Credential.provide_for(provider_name)
    # 認証プロバイダに対応するSTIクラス（TwitterCredentialとかFacebookCredentialとかを返す）
    if provider_class = Credential.provide_for(omniauth[:provider])
      # ユーザーとアクセストークンを関連付ける。同一プロバイダの古いトークンがあれば削除。
      # （アクセストークンはサーバで持って置いて必要な時や名寄せの時に使います）
      provider_class.from_user_omniauth(
          user: user,
          omniauth: omniauth)
    else
      Rails.logger.error "Unsupported provider: #{omniauth[:provider]}"
    end

    # ログイン中のユーザーをセッションに入れておく
    session[:user_id] = user.id

    redirect_to root_url
  end

  def logout
    session.delete(:user_id)
    redirect_to root_url
  end

  private
  def omniauth
    request.env['omniauth.auth']
  end
end

```

## 感想

Cognitoを使うことで、割と面倒な名寄せを上手く実装できた気がします。

Tokyoリージョンはありませんが、用途的に頻繁にアクセスするものではないので、個人的には多少のレイテンシの高さは気にならないかなと思っています。