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

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


# 参考文献

- 公式サイト
 - [サインイン後に API Gateway および Lambda を使用してリソースにアクセスする](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-accessing-resources-api-gateway-and-lambda.html)
 - [Amazon Cognito ユーザープール をオーソライザーとして使用して REST API へのアクセスを制御する](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html)


# APIの作成
早速APIを作成していきます。AWSマネージメントコンソールからLambdaを開き、__関数の作成__を押下します。
![lambda1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/25ef694d-505e-dd10-4f9d-525b8f47ce6e.jpeg)

まず、__設計図の使用__を選択し、__http__でフィルタします。microservice-http-endpoint-pythonを選択します。python版の選択理由は単に筆者の職場では、Lambdaのアプリをpythonで書いていて慣れているからです。
![lambda2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/55e7af53-bba7-d61d-a058-c72ccc07838f.jpeg)

__関数名__には任意の名前を入れてください。__実行ロール__は、__基本的なLambdaアクセス権限で新しいロールを作成__を選択します。
![lambda3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/c30ae3b8-91b9-56c6-6f35-1407a3d067c2.jpeg)

``API Gaeway トリガー``では__API__に__Create an API__、__API Type__に__REST__、__セキュリティ__に__オープン__を選択します。__API名__は任意に決めてください。入力が終わったら__追加ボタン__を押下します。
![lambda4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/7b8d9582-7203-49dc-6eb0-f5911e222cf0.jpeg)

Lambda関数ができるので、関数のコードを書き換えます。
![lambda5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/51deda17-3751-c634-1b56-d3e886adde60.jpeg)

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

``` python
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が表示されるので選択します。

![lambda6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/088dc8e7-1f61-3649-a384-090ab8ae77e8.jpeg)

__オーソライザー__を選択し、__新しいオーソライザーの作成__を押下します。__名前__には任意の名前を入力し、__タイプ__は__Cognito__を選択します。__Cognitoユーザープール__では、作成してあるプールを選択し、__トークンのソース__に__Authorization__を入力し、__作成ボタン__を押下します。
![apigw0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/78a6af9d-58b8-75d8-d1ec-e4288aeb39b5.jpeg)
__リソース__を選択し、作成したAPIの__ANY__を選択し、メソッドリクエストを選択します。
![apigw1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/1adcadce-a5cb-5df1-2219-53ac65466e47.jpeg)

__認可__で``Cognitoユーザープールオーソライザー``で先ほど作成したオーソライザーを選択します。
![apigw2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/d066db33-0213-8dc2-b803-c387e5a416e6.jpeg)

__アクション__から__APIのデプロイ__を選択し、__デプロイされるステージ__で__default__を選択し__デプロイボタン__を押下します。
![apigw3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/61422f8f-0140-fe28-6dd9-d3b242dc02a1.jpeg)


![apigw4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273926/a3cb4247-e1a5-4e87-1b79-3500ef8fa44c.jpeg)

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

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

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