FlutterとAWSで始めるサービス開発 (8)Cognitoの認証情報を使ってAPIを呼び出す


はじめに

(5)AWS Cognitoでログイン」や、「(7)AWS Cognito Googleでログイン」で、Cognitoを使ったログイン処理を一通り実装完了しました。今回はそれら認証情報がないと呼び出せないAPIを実装していきたいと思います。前回まででCognito ユーザープールからID Tokenを取得するところまではできています。APIを呼び出すために、ID Tokenを要求し、それを検証してから実行するAPIを作っていきます。つまり、ログインした場合だけサービスのAPIを呼び出せるというモデルを実現するための構成です。認証した上でさらに特定の権限を持っているユーザーだけが利用できるといったAPIも考えられますが、今回はそこまでは踏みこみません。

参考文献

APIの作成

早速APIを作成していきます。AWSマネージメントコンソールからLambdaを開き、関数の作成を押下します。

まず、設計図の使用を選択し、httpでフィルタします。microservice-http-endpoint-pythonを選択します。python版の選択理由は単に筆者の職場では、Lambdaのアプリをpythonで書いていて慣れているからです。

関数名には任意の名前を入れてください。実行ロールは、基本的なLambdaアクセス権限で新しいロールを作成を選択します。

API Gaeway トリガーではAPICreate an APIAPI TypeRESTセキュリティオープンを選択します。API名は任意に決めてください。入力が終わったら追加ボタンを押下します。

Lambda関数ができるので、関数のコードを書き換えます。

以下が、変更後のコードです。一部元のソースを流用していますが、ほぼ書き換えてしまっています。本実装では、呼び出し時に渡されたeventの一部(event['requestContext']['authorizer']['claims'])を返却することだけをしています。event['requestContext']['authorizer']['claims']に関しては後ほど解説したいとおもいます。

import boto3
import json

print('Loading function')


def respond(err, res=None):
    return {
        'statusCode': '400' if err else '200',
        'body': err.message if err else json.dumps(res),
        'headers': {
            'Content-Type': 'application/json',
        },
    }


def lambda_handler(event, context):

    response =  {
            "claims" : event['requestContext']['authorizer']['claims']
        }

    return respond(None, response)

以上で、APIの骨格が完成しましたが、次に、APIにCognitoとの認証連携を設定していきます。マネージメントコンソールでAPI Gatewayを開くと、先ほど作成したAPIが表示されるので選択します。

オーソライザーを選択し、新しいオーソライザーの作成を押下します。名前には任意の名前を入力し、タイプCognitoを選択します。Cognitoユーザープールでは、作成してあるプールを選択し、トークンのソースAuthorizationを入力し、作成ボタンを押下します。

リソースを選択し、作成したAPIのANYを選択し、メソッドリクエストを選択します。

認可Cognitoユーザープールオーソライザーで先ほど作成したオーソライザーを選択します。

アクションからAPIのデプロイを選択し、デプロイされるステージdefaultを選択しデプロイボタンを押下します。

以上でサーバー側の設定が完了です。

APIの呼び出し

ここまでで作ったAPIをFlutterのアプリケーションから呼び出します。
(5)AWS Cognitoでログインで作成したトップ画面を修正します。
まず、画面にAPIの呼び出し結果をテキストで表示する機能を追加します。非同期処理になるので、FutureBuilder<>を利用します。FutureにAPIを呼び出し、レスポンスを文字列で返却する関数である_invokeApi()を設定します。builderではデータを受け取ったら、API呼び出し結果をTextで返却、データを受け取らない間は、CircularProgressIndicator()で処理中を表示するようにしています。
引き続き、_invokeApi()の中身を確認します。先ほど作成したAPIの呼び出しコードです。Autorizationヘッダに、(5)AWS Cognitoでログイン(7)AWS Cognito Googleでログインで取得した、CognitoUserSessionインスタンスの、JWT形式IDトークンを設定しGET呼び出しを行っています。また、APIの呼び出し結果のResponse.bodyを戻り値としています。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('トップページ'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('ログイン成功'),
            Divider(color: Colors.black),
            FutureBuilder<String>(
                future: _invokeApi(session),
                builder: (context, AsyncSnapshot<String> snapshot) {
                  if (snapshot.hasData) {
                    return Text(snapshot.data);
                  } else {
                    return CircularProgressIndicator();
                  }
                })
          ],
        ),
      ),
    );
  }

  Future<String> _invokeApi(CognitoUserSession session) async {
    String url =
        "https://2wmzck1189.execute-api.ap-northeast-1.amazonaws.com/default/testCognito";
    final response = await http.get(url,
        headers: {'Authorization': session.getIdToken().getJwtToken()});
    if (response.statusCode != 200) {
      throw Exception("Received bad status code from API:" +
          response.statusCode.toString() +
          "; body: " +
          response.body);
    }

    return response.body;
  }
}
``

実際に本アプリを実行すると以下のように画面にレスポンスが表示されます。

![app1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/5bae9119-23dc-8c2f-e282-6d89cbb36ef3.jpeg)


以上で、最低限のアプリケーションは完成です。

# API側での処理
API GatewayのオーソライザーでCognitoID Tokenが検証できた場合、Lambda側には検証結果の情報として、Cognitoユーザーの情報がわたってきます。
前述した``event['requestContext']['authorizer']['claims']``にID Tokenの検証結果としてログインしているユーザーの情報が入ります。以下が具体的な、cliamsの中身になります。__sub__Cognitoのユーザープール内でユーザーを一意に識別するIDがわたってくるので、アプリケーションはそのキーを使って、ユーザーごとの処理を実装していくことが可能です。

``` json
"claims": {
  "at_hash": "jC8Q3e2kh1LmzloesriHHw",
  "sub": "bdec0872-9384-453f-99f9-c8c50dee23db",
  "aud": "***********************",
  "cognito:groups": "ap-northeast-1_*********_Google",
  "identities": "{"dateCreated":"1594465378058","userId":"100299533705733233603","providerName":"Google","providerType":"Google","issuer":null,"primary":"true"}",
  "token_use": "id",
  "auth_time": "1595514490",
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_*********",
  "cognito:username": "google_100299533705733233603",
  "exp": "Thu Jul 23 15:28:10 UTC 2020",
  "iat": "Thu Jul 23 14:28:10 UTC 2020"
}

まとめ

Cognitoでログインして取得したIDトークンを利用してAPIの呼び出しを実施するところまでできました。これでサービス開発の最低限の流れはできたと思います。本シリーズはいったんここまでで終わりたいと思います。ここまで実装したコードを一通りリファクタリングしたり、エラー処理を拡充して、もう少し人に見せられそうなレベルに持っていけたらソースコードを公開しようかなと思います。また、あまり技術的なことは深く突っ込まず、どう設定すれば動くのかに比重をおいて進めてきましたが、もう少し技術面を整理してその辺の情報も公開できれば名智考えています。