FlutterとAWSで始めるサービス開発 (7)AWS Cognito Googleでログイン

# はじめに
前回の「[(6)AWS Cognito 「パスワードを忘れた方はこちら」](https://qiita.com/makotomi/items/d1a2fadb6bc9be39f21e)」までで、Cognitoを使ったログイン処理を一通り実装しました。今回は、サードパーティーによるログインをおこないたいと思います。サービスを利用しようと考えているユーザーはわざわざサービス専用にログインアカウントを作る必要がないですし、ID/PWD管理も一元化されて楽になるので利用開始の障壁を低くすることができます。

AWSのCognitoでは、Facebook,Amazon,Google,Appleといった主要な認証プロバイダーを標準でサポートしていますが、今回は、Googleを利用するパターンで開発していきたいと思います。

# 参考文献

## 公式サイト
- [サードパーティー経由のユーザープールへのサインインの追加](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-identity-federation.html)
- [Amazon Cognito ユーザープールで Google をフェデレーティッド ID プロバイダーとして設定する方法を教えてください。](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cognito-google-social-identity-provider/)

## amazon_cognito_identity_dart_2
- https://pub.dev/packages/amazon_cognito_identity_dart_2 > Usage > Use Case 19.

# Googleの設定 その1
まずは、ID連携用にGoogle側の準備をします。[Google Developer Console](https://console.developers.google.com/apis/dashboard)を開きあなたのGoogleアカウントでログインして登録を完了させてください。

__認証情報__ > __OAuth同意画面__を選択します。__アプリケーション名__を入力し__作成ボタン__を押下します。
![google_0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/b0c0f332-9243-c59b-ce4a-f01b84c5ab60.jpeg)


__認証情報__ > __認証情報を作成__ > __OAuthクライアントID__ を選択します。今回利用するGoogleでのログインでは``OpenID Connect``という``OAuth2``を拡張したプロトコルに基づいています。ここで発行するOAuthのクライアントはCognitoからGoogleを呼び出すときに利用するIDになりますので、後ほどCognito側に登録することになります。

![google_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/867c1076-b7ae-6523-2df9-870160f96644.jpeg)

__アプリケーションの種類__で__ウェブアプリケーション__を選択し、任意の__名前__を入力し、__作成ボタン__を押下します。後ほど承認済みのリダイレクトURIを入力するのですが、このタイミングではまだ決まっていなので未入力で進めます。

![google_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/62ab488a-5c53-26f5-b7cb-b55cd2e8a704.jpeg)

作製が完了すると以下の画面が表示されます。クライアントIDとクライアントシークレットをCognitoに設定する必要があるので控えてください。

![google_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/265d109b-c363-63ae-69e9-355e8fa5b6e5.jpeg)

# Cognitoの設定

いったんAWS Cognito側の設定に移動します。マネージメントコンソールよりCognitoのユーザープールを開いてください。__フェデレーション__ > __IDプロバイダー__を開き、__Google__を選択します。先ほどGoogleで作成したクライアントIDを__Google アプリ ID__に、クライアントシークレットを__アプリシークレット__に入力します。また、__承認スコープ__に__profile email openid__を入力して、__Google有効化ボタン__
を押下します。
![cognito_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/e9e411ad-274b-b920-db46-b0cb1036a27a.jpeg)


次に、__アプリの統合__ > __アプリクライアントの設定__を開きます。__有効なIDプロバイダ__で__Google__をチェック、__サインインとサインアウトの URL__ > __コールバック URL__に__myapp://__を入力、__OAuth 2.0__ > **許可されている OAuth フロー** で__Authorization code grant__をチェック、**許可されている OAuth スコープ**で__email__と__openid__と__aws.cognito.signin.user.admin__と__profile__をチェックし__変更の保存ボタン__を押下します。

![cognito_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/40a7500e-4481-875f-99fa-6bf900cada28.jpeg)

次に、__アプリの統合__ > __ドメイン名__を開きます。今回は、__Amazon Cognito ドメイン__を使うパターンで進めます。__ドメインのプレフィックス__にお好きなドメイン名を設定し、__使用可能かチェックするボタン__を押下します。

なお、「[AWSのみで構築する独自ドメイン/SSL/WordPressサイト](https://qiita.com/makotomi/items/56b6f0a49b11c09ccdc9)」で紹介したような形で独自ドメインを取得しAWSで証明書を発行していれば、__自分が所有するドメイン__側もすぐ設定できます。``このドメインは利用できます``と表示されたら、__変更の保存ボタン__を押下します。ここで作成したURLをGoogle側に登録するので控えておいてください。

![cognito_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/05bbb6e9-177b-2cfa-557d-b4bf9310873b.jpeg)

# Googleの設定 その2
Cognitoの設定が完了したら、再度Googleの設定に戻ります。__認証情報__ > __OAuth 2.0 クライアント ID__から先ほど作成したクライアントを開きます。__承認済みのリダイレクト URI__にCognitoで設定したURLに__/oauth2/idpresponse__を付与した、
「__https://[入力したドメイン].auth.ap-northeast-1.amazoncognito.com/oauth2/idpresponse__」を入力して__保存ボタン__を押下してください。

![google_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/38b90e2d-f3ca-7707-57f4-9c73c17e8009.jpeg)

# モバイルアプリへの組み込み

## MaterialAppのroutesにGoogleでログイン画面を追加

__'/LoginByGoogle'__という名前で定義を追加しました。

```dart
      routes: <String, WidgetBuilder>{
        '/': (_) => new MyHomePage(),
        '/TopPage': (_) => new TopPage(),
        '/RegisterUser': (_) => new RegisterUserPage(),
        '/ConfirmRegistration': (_) => new ConfirmRegistration(null),
        '/ForgotPassword': (_) => new ForgotPassword(),
        '/LoginByGoogle': (_) => new LoginByGoogle(),
      },
```

## ログイン画面の変更

Googleでログインボタンを一番下に追加しています。押下するとGoogleでログイン画面に飛びます。またまたデザインは後回しです。


``` dart
            Divider(color: Colors.black),
            RaisedButton(
              child: Text('Googleでログイン'),
              color: Colors.indigo,
              textColor: Colors.white,
              shape: StadiumBorder(),
              onPressed: () =>
                  Navigator.of(context).pushNamed('/LoginByGoogle'),
            ),
```

実装した画面は以下になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/e73e6b61-b102-c1b2-1a9b-a6102d2fd62a.jpeg" width="50%" />

## Googleでログイン画面

WebViewのコンポーネントを使ってGoogleのログイン画面を表示します。WebViewのコンポーネント自体はいくつか種類がありますが、Flutterの公式を利用しました。SDKのサンプルコードもそうですし、長い目で見れば公式というのは安心感があります。その他、必要なライブラリを一通りimportをします。

``` dart
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:http/http.dart' as http;
import 'package:webview_flutter/webview_flutter.dart';
```

次に画面です。__WebView__を使い、``initialUrl``にCognitoの認証エンドポイントとして開くよう指定します。Ｃognitoユーザープールの認証エンドポイントURLにはCognitoのユーザープールID（COGNITO_POOL_URLに格納)、クライアントのID(COGNITO_CLIENT_IDに格納)の値が必要になります。なお、Googleで発行したクライアントではなく、Cognitoで発行したクライアントになりますのでご注意ください。
次に、``useragent``には正規のデバイスが送信する文字列と同じ値を設定します(SDKサンプルからのコピペですが、LG Nexus 5の設定値らしいです)。これを設定しないとGoogleが正しくログイン画面を表示してくれず、「変なデバイスでアクセスするな!」といった感じでエラーが表示されます。
最後に、``navigationDelegate``に画面遷移した時の処理を書きます。ざっくりとCognito認証エンドポイント → Googleログイン画面 → 端末(``myapp://``)といった画面遷移をします。``myapp://``に戻ってきたときにGoogleから渡される``Authoraization Code``がパラメータとして入るので、``myapp://``が開いたとき、かつ、クエリパラメータに``code``を持つときという条件で次の処理に進むようにしています。SDKのサンプルではこの辺の実装が適当なのできちんと__Uriクラス__を使ってUrlをパースして処理するように書き換えています。


``myapp://``でコードを取得した後は、**_signUserInWithAuthCodeメソッド**で認証セッションを取得し、__Navigator.of(context).pushReplacementNamed('/TopPage');__でログイン結果画面に飛びます。この画面は[(5)AWS Cognitoでログイン](https://qiita.com/makotomi/items/a0b05e8819270072780b)で作成した画面と共通です。

**_signUserInWithAuthCodeメソッド**では、Cognitoユーザープールのトークンエンドポイントにリクエストを投げてセッションを確立します。トークンエンドポイントURLには、先ほどと同様CognitoのユーザープールID、クライアントのIDの値と、Googleから取得した``Authoraization Code``が必要になります。トークンエンドポイントを呼び出した結果を使い、__CognitoUserSession__をインスタンス化します。これでCognitoでログインした場合と同様の状態になります。

``` dart
class LoginByGoogle extends StatelessWidget {
  final Completer<WebViewController> _webViewController =
      Completer<WebViewController>();

  @override
  Widget build(BuildContext context) {
    // Googleのログイン画面を毎回出してテストするためにCookieをクリア
    CookieManager().clearCookies();
    return Scaffold(
        appBar: AppBar(
          title: Text('Googleでログイン'),
        ),
        body: Center(
            child: WebView(
          initialUrl: "https://$COGNITO_POOL_URL" +
              ".amazoncognito.com/oauth2/authorize?identity_provider=Google&redirect_uri=myapp://&response_type=CODE&client_id=$COGNITO_CLIENT_ID" +
              "&scope=email+openid+profile+aws.cognito.signin.user.admin",
          userAgent: 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) ' +
              'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Mobile Safari/537.36',
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (WebViewController webViewController) {
            _webViewController.complete(webViewController);
          },
          navigationDelegate: (NavigationRequest request) async {
            if (request.url.startsWith("myapp://")) {
              var uri = Uri.parse(request.url);
              if (uri.queryParameters.containsKey('code')) {
                try {
                  session = await _signUserInWithAuthCode(
                      uri.queryParameters['code']);
                  Navigator.of(context).pushReplacementNamed('/TopPage');
                } catch (e) {
                  print(e);
                }
              }
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
          gestureNavigationEnabled: true,
        )));
  }

  Future _signUserInWithAuthCode(String authCode) async {
    String url = "https://$COGNITO_POOL_URL" +
        ".amazoncognito.com/oauth2/token?grant_type=authorization_code&client_id=" +
        "$COGNITO_CLIENT_ID&code=" +
        authCode +
        "&redirect_uri=myapp://";
    final response = await http.post(url,
        body: {},
        headers: {'Content-Type': 'application/x-www-form-urlencoded'});
    if (response.statusCode != 200) {
      throw Exception("Received bad status code from Cognito for auth code:" +
          response.statusCode.toString() +
          "; body: " +
          response.body);
    }

    final tokenData = json.decode(response.body);

    final idToken = new CognitoIdToken(tokenData['id_token']);
    final accessToken = new CognitoAccessToken(tokenData['access_token']);
    final refreshToken = new CognitoRefreshToken(tokenData['refresh_token']);
    return new CognitoUserSession(idToken, accessToken,
        refreshToken: refreshToken);
  }
}
```

実装した画面は以下になります。Cognitoの認証エンドポイントは実際に画面を表示せずGoogleログイン画面にリダイレクトされるので、Googleのログイン画面が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/edfd9da6-9d6d-9c4c-1109-b5aca2b816e3.jpeg" width="50%">

# まとめ
Googleでログインし、フェデレーションすることができました。Cognitoは他にも色々なサードパーティーのIdPをサポートしています。他のプロバイダでのログインも同じような流れで導入できると思います。繰り返しますが、エンドユーザー的にはあれこれ考えず、すでに使っているサービスのIDとパスワードでログインするだけで新しいサービスが使い始められます。「IDを登録する」というサービス導入の障壁を取り除けるので積極的にサポートしていくのが良いかと思います。次回は「[(8)Cognitoの認証情報を使ってAPIを呼び出す](https://qiita.com/makotomi/items/1db93a3ef0c63fc37325)」とし、認証した結果をもとに実際のサービスのAPIをたたくところの処理を見ていきたいと思います。