Help us understand the problem. What is going on with this article?

AWS CognitoでFacebookログインを実装してみた

こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。

弊社は、株式会社Ancar様のサービス「Ancar」の開発をサポートしており、僕も現在関わらせていただいています。

Ancarでは、既存の認証機能をCognitoへ移行することを目指しており、そこで得た知見が、少しばかりではありますが、あるので備忘録として残すことにしました。少しでも皆さんのお役に立てたら嬉しく思います。

目次

  • Cognito移行の背景
  • AWS CognitoでFacebookログインを実装する
  • 同じemailのユーザーが二人出来てしまう問題を解決する
  • まとめ

Cognito移行の背景

Ancarは、中古車の個人間売買サービスです。中古車売買において“安心安全な個人間売買”の実現を目指し、日々サービス開発を行っております。

スクリーンショット 2019-11-26 20.00.09.png

また9月に中古車のアグリゲーションサービス「Ancar Search」を、10月にAncarへの出品車両を対象にしたカメラアプリをリリースしました。

今後も新たなサービスを続々とリリースすることを予定しており、そこで課題にあがったのが認証機能でした。

展開するサービスが増えた時に、サービスごとに別々のユーザーを作るのではなく、複数サービス間でも1つのユーザーで認証を行えるのが理想だろうという考えになったのです。

それを実現するためには、1つ、基盤となるユーザー認証基盤を据える必要があり、そのような背景からAWS Cognitoへの移行を決めました。

今回の記事では、Cognito移行の中でも苦戦したCognito Facebookログインへの移行について書きました。ちなみに記事中にある記述例は、言語はPHPで、フレームワークはSymfonyで書いています。

AWS CognitoでFacebookログインを実装する

事前準備

この記事は、あくまでCognitoでのFacebookログインについて書きたいと思います。ですので、上記の事前準備については、他のドキュメントを参考にしていただきたいです。

Cognito Facebookログインフロー

Cognito Facebookログインのフローをざっくりとまとめると、このようになります。

  1. 認可エンドポイントにリクエストし、Cognito経由でFacebookログインを行う
  2. トークンエンドポイントにリクエストし、ユーザープールからアクセストークンを取得する
  3. 取得したアクセストークンを用いて、userInfoエンドポイントにリクエストし、Cognito Facebookログインを行ったユーザーの情報を、ユーザープールから取得する
  4. ユーザープールから取得したユーザーの情報を自前DBと照合し、該当するユーザーの認証をアプリケーション側で通す

では、次から実際に上記のフローをどのように実現しているのかを見ていきたいと思います。

(1)Cognito Facebookログインを行うURLを生成する

こちらがCognito Facebookログインの流れになります。

scenario-authentication-social.png
出典:AWSドキュメント

アプリからURLにアクセスすると、Cognitoを経由してFacebookの認証を行い、またCognitoを経由してレスポンスが返ってくるようになっています。

では、早速Cognito Facebookログインを実行するURLから見ていきたいと思います。

Cognito Facebookログインでは、 認可エンドポイント /oauth2/authorize にパラメータを付与してこのようにリクエストを送ります。

https://COGNITO_DOMAIN.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?response_type=code&client_id=COGNITO_CLIENT_ID&redirect_uri=REDIRECT_URI&identity_provider=Facebook&state=STATE&scope=openid+profile+email

各パラメータの内容は、このようになっています。

パラメータ名 内容
response_type レスポンスのタイプ
client_id CognitoユーザープールのクライアントID
redirect_uri 認証後にリダイレクトされるURL
identity_provider 利用するプロバイダ名、ここではFacebook
state 初期リクエストに追加する OPAQUE 値
scope アクセストークンがアクセスできる値を指定

パラメータを付与したこの認可エンドポイントにリクエストを送ると、おなじみのFacebookログイン画面が表示されます。

スクリーンショット 2019-11-29 20.04.03.png

この画面でログインを許可すると、さっそくCognitoのユーザープールにユーザーが作成されました。
cognitofb.png

これでCognitoへのFacebookログイン自体はできるようになりました。

Cognitoにログインしたユーザーを、アプリケーション側でも認証を通すためにも、CognitoでFacebookログインしたユーザーが、自前DBのどのユーザーなのかを判別できなければいけません。

そのために、CognitoでFacebookログインしたユーザーの情報をアプリケーション側へ引っ張ってきたいと思います。

先ほど、認可エンドポイントに対して、パラメータ response_type=code を付与して、リクエストしました。そのレスポンスとして、URLに以下のような形式で、 code が返ってきます。

https://YOUR_APP/REDIRECT_URI?code=AUTHORIZATION_CODE&state=STATE

response_typeにcodeを指定すると、認証コードを提供します。このコードは「トークンエンドポイント」を使用してアクセストークンと交換できます。

参考:認可エンドポイント | AWSドキュメント

(2)トークンエンドポイントにリクエストして、アクセストークンを取得する

では先ほど、認可エンドポイントからのレスポンスとして返ってきた、codeを用いてユーザーの情報を引っ張っていきたいと思います。こちらのcodeを付与して、トークンエンドポイントにリクエストすると、アクセストークンを取得することができます。

cognito.php
        $baseUrl = 'https://COGNITO_DOMAIN.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?';

        $client = new \GuzzleHttp\Client([
            'base_uri' => $baseUrl,
        ]);

        $tokenEndpointPath = '/oauth2/token';
        $code = $request->query->get('code');

        if (!$code) {
            throw new \RuntimeException('No code');
        }

        $tokenEndpointResponse = $client->request(
            'POST',
            $tokenEndpointPath,
        [
            "headers" => [
                "Content-Type"  => "application/x-www-form-urlencoded",
            ],
            "form_params" => [
                "grant_type"   => 'authorization_code',
                "client_id"    => COGNITO_APP_CLIENT_ID,
                "code"         => $code,
                "redirect_uri" => REDIRECT_URI,
            ],
        ]
        );

これでCognito Facebookログインでログインした、Cognitoユーザープールのユーザーのアクセストークンを取得できました。

では、ついにこのアクセストークンを用いて、ユーザープールの情報を引っ張っていきます。

参考:トークンエンドポイント | AWSドキュメント

(3)userInfoエンドポイントにリクエストして、Cognitoにログインしたユーザーの情報を取得する

では、先ほど取得したアクセストークンを付与した、userInfoエンドポイントにリクエストしてみます。

cognito.php
        $userInfoEndpointPath = '/oauth2/userInfo';
        $token = $tokenEndpointResponseData['access_token'];

        if (!$token) {
            throw new \RuntimeException('No access_token');
        }

        $userInfoEndpointResponse = $client->request(
            'GET',
            $userInfoEndpointPath,
        [
            "headers" => [
                'Authorization' => 'Bearer ' . $token,
                "Content-Type"  => "application/x-www-form-urlencoded",
            ],
        ]
        );

レスポンスとしてこのような形式で返ってきます。これで無事にユーザープールから、Cognito Facebookログインしたユーザー情報を取得してこれました。

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
   "sub": "248289761001",
   "name": "test test",
   "email": "test@example.com",
   "username": "Facebook_000000000000",
}

あとは、アプリケーション側で返ってきたユーザーを自前のDBと照合して、新規登録orログインさせればOKです。

参考:USERINFO エンドポイント | AWSドキュメント

同じemailのユーザーが二人出来てしまう問題を解決する

Cognito導入予定のサービスには、Facebook認証に加え、メールアドレス/パスワードによる認証を用意しています。そのため、Cognito Facebookログインに加えて、メールアドレス/パスワードによるCognito認証フローも用意しました。

メールアドレス/パスワードによるCognito認証フローの実装を終え、Cognito Facebookログインの実装を始めたところで、想定外の問題が発生したのです。

問題となったのは、メールアドレス/パスワードでのCognito認証フローを通ったユーザーが、ログアウト後にCognito Facebookログインを行った場合、メールアドレス/パスワード認証のCognitoユーザー、Cognito FacebookログインのCognitoユーザー、というように同じemailのCognitoユーザーがユーザープールに二人できてしまったことです。

cognitoemail,png.png

emailで絞り込むと、ユーザーが2人いる...。

さてどうしたものかと、しばらく頭を抱えていましたが、調べていく中でCognitoのAPIで AdminLinkProviderForUser というものがあることを知りました。

このメソッドは、Cognitoのユーザープールに存在するユーザーと、それとは別のプロバイダ経由で認証したユーザーをリンクさせるものです。

cognito.php
$client->adminLinkProviderForUser([
    'DestinationUser' => [
        'ProviderAttributeValue' => COGNITO_USER_NAME,
        'ProviderName'           => "Cognito",
    ],
    'SourceUser' => [
        'ProviderAttributeName'  => 'Cognito_Subject',
        'ProviderAttributeValue' => FACEBOOK_USER_ID,
        'ProviderName'           => "Facebook",
    ],
    'UserPoolId' => COGNITO_USER_POOL_ID,
]);

これを実行すると、一覧では依然として、メールアドレス/パスワード認証のCognitoユーザー、Cognito FacebookログインのCognitoユーザーの2人が存在するのですが、 AdminLinkProviderForUser 実行後には詳細ページでは同じ情報が表示されるようになり、1人のユーザーとしてメールアドレス/パスワード認証もCognito Facebookログインもできるようになります。

参考:AdminLinkProviderForUser | AWS Documentation

まとめ

Cognitoの実装例は、ネイティブアプリが多く、特にFacebookログインとなると日本語の記事では、公式ドキュメント以外にはほとんどないという状況で、実装方法にたどり着くまでにかなり時間がかかってしまいました。

公式ドキュメントを読んでも分からないことが多く、何度か目黒のAWS Loftにも足を運び、Ask An Expert カウンター(Loftにて、AWSのプロダクトやソリューションを熟知したエンジニアに、技術的な相談ができる場)で相談させていただいたりもしました。その節は大変お世話になりました。

今回の備忘録が、少しでも皆さんのお役に立てれば嬉しいです。

最後に

よく分からない点や間違っている点などがありましたら、コメント欄で教えていただければと思います。

またCognitoに関する質問も、僕にお答えできる範囲でありましたら、お力になれればと思っております。そちらもコメント欄でお願いいたします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away