5
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

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

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

参考文献

公式サイト

amazon_cognito_identity_dart_2

Googleの設定 その1

まずは、ID連携用にGoogle側の準備をします。Google Developer Consoleを開きあなたのGoogleアカウントでログインして登録を完了させてください。

認証情報 > OAuth同意画面を選択します。アプリケーション名を入力し作成ボタンを押下します。
google_0.jpg

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

google_1.jpg

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

google_2.jpg

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

google_3.jpg

Cognitoの設定

いったんAWS Cognito側の設定に移動します。マネージメントコンソールよりCognitoのユーザープールを開いてください。フェデレーション > IDプロバイダーを開き、Googleを選択します。先ほどGoogleで作成したクライアントIDをGoogle アプリ IDに、クライアントシークレットをアプリシークレットに入力します。また、承認スコープprofile email openidを入力して、Google有効化ボタン
を押下します。
cognito_1.jpg

次に、アプリの統合 > アプリクライアントの設定を開きます。有効なIDプロバイダGoogleをチェック、サインインとサインアウトの URL > コールバック URLmyapp://を入力、OAuth 2.0 > 許可されている OAuth フローAuthorization code grantをチェック、許可されている OAuth スコープemailopenidaws.cognito.signin.user.adminprofileをチェックし変更の保存ボタンを押下します。

cognito_2.jpg

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

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

cognito_3.jpg

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

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

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

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

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

ログイン画面の変更

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

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

実装した画面は以下になります。

Googleでログイン画面

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

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の認証エンドポイントとして開くよう指定します。Cognitoユーザープールの認証エンドポイント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でログインで作成した画面と共通です。

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

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のログイン画面が表示されます。

まとめ

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

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
5
Help us understand the problem. What are the problem?