Flutterアプリのユーザー認証システムにAmazon Cognitoを使う(OAuth 2, Google, LINEなど)

**Special Thanks! : @to-jiki と共同でこの記事を作成しました.**

**結構長いのでご注意！**

Flutterを使ったアプリで, ユーザー認証のシステムを作りたいと思い, バックエンドをAWSで実装する必要があったため, Amazon Cognitoを使うことになりました. ただ, Flutter+Firebaseの情報は結構出てくるのですが, Flutter+Amazon Cognitoの方があまり出てこなくて困ったのでメモ程度にまとめたいと思います. 個人的に結構困った, **Flutter部分も結構詳しめに書いています.**

1つ注意していただきたいのが, FlutterもAWSも初心者の状態でいろいろ手探りなため, 正直遠回しなところや, よくない事をしているところもあるかもしれません. 参考程度にとどめて, あくまで自己責任でやっていただきたいと思います.

また, この記事ではバックエンドとの通信はAPI Gateway+Lambdaを用いたフルサーバーレスなアプリケーションの作成を想定しています([furaiev/amazon-cognito-identity-dart-2](https://github.com/furaiev/amazon-cognito-identity-dart-2/#for-api-gateway--lambda)).

その他のアーキテクチャの場合は, [furaiev/amazon-cognito-identity-dart-2](https://github.com/furaiev/amazon-cognito-identity-dart-2)のReadmeをご覧ください.


**以下, リージョンはすべてus-east-1でやっていますが, そこは適宜読み替えてください.**

# Amazon Cognitoの設定
AWSのアカウントの作成等の作業はここでは省略します．IAMユーザーを作って作業をすることは忘れずに:bird:
ここでは

* メールアドレスとパスワードによる認証
* Googleアカウントによる認証
* LINEアカウントによる認証

のCognito及び関連(GCP,LINE Developer)の設定方法についてそれぞれ紹介します．

>上記二つの設定の大半はmakotomiさんの[記事](https://qiita.com/makotomi/items/cb6ccb8568eacf8772dd)を参考にさせていただきました．似たような内容になってしまったのですが細かい設定や，後に使用したプラグインなどで異なる部分はあるので参考にされる方々は両方目を通していただけると幸いです．

## メールアドレスとパスワードによる認証
### ユーザープールの作成
---
初めに認証したユーザーを保存しておくユーザープールを作成します．これはGoogle，LINEによる認証でも使用します.

(1) ユーザープール名を決め，**デフォルトを確認する**を選択します．

![](https://i.imgur.com/ZRW6vNv.png)

(2) **属性**で設定したい認証方法に沿った項目を設定します．(今回は、Eメールアドレスおよび電話番号)
ここの設定はプール作成をしてしまうと**変更することはできない**ので，よく確認してください．
また下の方にある標準属性でemailを選択するとうまく動かないことがあります．

![](https://i.imgur.com/4sy7aer.png)

(3) **アプリクライアントの作成**をしてアプリごとに紐付けを行うID(アプリクライアントID）を発行します．ユーザープール作成後に表示されます．
クライアントシークレットは外して，あとはお好みで設定してください．


![](https://i.imgur.com/iniL62e.png)


(4) ポリシー等でパスワードの条件等を設定できるので欲しい要件にあった設定を行いプールの作成を押してください．

(5) 作成後，後で必要になるプールIDとアプリクライアントIDは控えておいてください．
![](https://i.imgur.com/2hBXQST.png)

![](https://i.imgur.com/eIM3qjc.png)

メールアドレスとパスワードによる認証のCognito側の準備は以上です．
コードの中でどのように使うかは設定を全部した後で話そうと思います．




## Googleアカウントによる認証
Googleとの認証の連携では**Google Cloud Platforｍ**を使用します．自分のGoogleアカウントを用いて登録してください．
　
(1) 初めにProjectの作成をします．
 ![](https://i.imgur.com/DiiId4L.png)
 
 
(2) **APIとサービス**➞**OAuth同意画面**でアプリケーションの作成を行います．
 設定はデフォルトで大丈夫です．
 ![](https://i.imgur.com/5luZtFr.png)
 
(3) **認証情報**➞**＋認証情報を作成**➞**OAuthクライアントID**を選択して,
 ウェブアプリケーションの種類と名前を決め作成します．
 
 ![](https://i.imgur.com/fOkanxI.png)
 
 
　～.apps.googleusercontent.comというクライアントIDとクライアントシークレットが作成されます．Cognitoとの連携で使用するのでメモしておいてください．
 
![](https://i.imgur.com/zzFzbZb.png)

(4) 一旦Cognitoのほうに戻ります．
ダッシュボードの下のほうにあるIDプロバイダーを開きGoogleを選択します．
ここで先ほどメモしたIDを使用します．

* GoogleアプリID → クライアントID
* アプリシークレット → クライアントシークレット
* 承認スコープ → **profile email openid**

のように入力をして，有効化を押します.
![](https://i.imgur.com/fBI9B7Q.png)

(5) **アプリクライアントの設定**の有効なプロバイダですべてを有効にして以下のように設定します．
コールバックURL(Flutter側の設定ではcallbackSchemeと呼んでいます)はログインの処理が終わった時の遷移先のことです. 今回の記事の中では`myapp://`としています.

> コールバックURLはCognito側とアプリ側で統一すればどんなものでも構いませんが, すこしややこしいので最後が`://`で終わるようにしてください. また, 本当は`myapp://`という簡易的なものではなく, ほかにスマホにインストールされているアプリのコールバックスキームと重複しない固有なものにするべきです. 今回はわかりやすさのため`myapp://`としていますが, 実際にアプリを作る際は, 一意なものになるようにしてください. 例えば, `jp.foo.bar.ci78mls2://`などランダムな文字列を入れたりすると良いかもしれません.

![](https://i.imgur.com/Bf3E6gd.png)

(6) **ドメイン名**を選択しプールの場所を示してくれるAmazon Cognito ドメイン作成します．使用可能なドメインかチェックをして変更の保存をしてください．

![](https://i.imgur.com/OvR98g2.png)

再びGoogleの設定へ戻ります．

(7) GCPの**APIとサービス** → **認証情報**で先ほど作成したクライアントの承認済みURLに，６で作成したドメインに/oauth2/idpresponseを付加した
`https://[自分で決めたドメイン].auth.us-east-1.amazoncognito.com/oauth2/idpresponse`
を入力して保存します．
![](https://i.imgur.com/2pF56dm.png)


以上でGoogelアカウントによるログインでのCognito，GCPの設定は以上です．

## LINEアカウントによる認証

LINEによる認証ではLINE Developer を使用します． 特別に何か登録する必要はないので自身のLINEアカウントでログインするだけで大丈夫です．

(1) 初めにプロバイダーを作成します．**LINE Login** → **Start now** → **Console home** を開きます．ここでは自分たちが実際に利用したプロバイダーがありますが，最初は何もない状態だと思いますので新しく作成してください．

![](https://i.imgur.com/t0A4Gkc.png)

(2) プロバイダーを開いたら**Create a LINE Login Channel**を選択します．
Channel Typeの選択では**LINE Login**を，以下のような入力画面が出てくるのでうちのApp typesでは**Web App** を選択．後の名前などは自分のプロダクトにあった名前を付けてください．
optional の項目は入力しなくて大丈夫です．

![](https://i.imgur.com/iVE915i.png)
![](https://i.imgur.com/aXexphw.png)

(3) createをした後表示される画面で**Channel ID**と**Channel secret**の値はまたCoginito側の設定をするときに必要なのでメモしておいてください．
![](https://i.imgur.com/pGs6Kth.png)
![](https://i.imgur.com/ju6iK2c.png)

(4) Basic settings の隣にあるLINE Loginの中にあるCallback URLにGoogleの時と同様に`https://[自分で決めたドメイン].auth.us-east-1.amazoncognito.com/oauth2/idpresponse`
を入力します．

![](https://i.imgur.com/YOfFiPV.png)
最後にDevelpoingをPublishedにしてLINE側の設定は終わりです．
Cognitoの設定に移動します．

![](https://i.imgur.com/9qzYtYz.png)


(5) IDプロバイダーを開いて今度はLINE専用のプロバイダはないので**OpenID Connect**選択します．クライアントIDとシークレットに4のLINEのChannelでメモしたIDとシークレットを入力．
**承認スコープ**には**profile mail openid**
**発行者**には https://access.line.me/ 
![](https://i.imgur.com/34BhUd3.png)

このままプロバイダーを作成しようとすると，

> 検出により結果は返されませんでした。発行者を確認し、もう一度検出を実行するか、以下の必須のフィールドを手動で追加します。

と表記されるので，

* 認証エンドポイント → https://access.line.me/oauth2/v2.1/authorize
* トークンエンドポイント → https://api.line.me/oauth2/v2.1/token
* ユーザ情報エンドポイント → https://api.line.me/v2/profile

を入力．
Jwks uriは特に入力する必要はないのですが，https:// から始まるURLを入れないとエラーが出るので適当に入れておきましょう．
![](https://i.imgur.com/AX8p7hd.png)

(6) 後は**アプリクライアントの設定**で有効なIDプロバイダで作成したLINEを選択して保存すれば設定は完了です．

### おまけ
後のアプリとの連携でつかうのでIDプールも作成しておきます．
認証プロバイダーにそれぞれメモしたIDを入力して作成プールの作成を行います．
![](https://i.imgur.com/Vrercqe.png)

作成後の画面でIDプールのIDが表示されるのでこれもメモしておきましょう．
![](https://i.imgur.com/HPIEhhp.png)

# Flutterの設定

ユーザー認証付きのアプリを作成する場合, ユーザーが登録・ログインしたタイミングでユーザーごとに一意なIDを取得し, それを使ってAPIコールをしたりすると思います.

ここでは, 登録・ログインしてから, 一意なユーザーIDを取得するところまでを解説します. また, おまけとして, Cognitoで認証されたユーザーのみがアプリ内からAPIコールを呼び出す方法についても見てみます.

**これ以下のコードでは例外は`try-catch`でキャッチして, `print()`しているだけですので, 必要な処理は書き足すようにしてください. また, `async-await`文も`await`の部分のみ書いているところも注意してください.**

また, これ以下で書いていることを使って簡単なユーザー認証アプリのデモを作ったので, もし良ければご覧ください. コードを見ながら, 実際に使いながらのほうがわかりやすいかもしれません.

https://github.com/Log4Rhythm/flutter_cognito_auth

ただし

* `auth.dart`にはダミーの文字列が入れてあるので, 実際に動かすには上記のCognitoで設定した各値が必要
* ログインに成功したりしても, トークンを`print()`したりするだけの簡易的なものなので, 続きは必要に応じて書いていく必要あり

であることに注意してください.

ユーザー認証のおおまかな流れは以下のようになります. 3種類のトークンについてはすぐ下に記載しています.

### メールアドレス・パスワード認証の場合

1. 登録・ログイン時に更新トークンを取得する.
2. 更新トークンを利用して, トークンエンドポイントから, 残り2種類のトークンを取得する. 
3. 一意なユーザーIDを取得する(IDトークンをデコードする or IDプールのIDを取得する).
4. 2で取得したIDトークンとアクセストークンを利用して, APIコールをする.


### サードパーティ認証の場合

1. 登録・ログイン時にAuthorization Codeを取得する. このコードは1回きりしか使用できない.
2. Authorization Codeを利用して, トークンエンドポイントから, 3種類のトークンを取得する. 
3. 一意なユーザーIDを取得する(IDトークンをデコードする or IDプールのIDを取得する).
4. 2で取得したIDトークンとアクセストークンを利用して, APIコールをする.

## 更新トークン・IDトークン・アクセストークン

Amazon Cognitoのトークンエンドポイントから取得できる3種類のトークンについては, 以下の記事などを参考にしてください.

* [Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する](https://dev.classmethod.jp/articles/study-tokens-of-cognito-user-pools/)
* [Refresh Token： どのような場合に使用し、どのように JWT と相互作用するか](https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/)
* [OAuth 2.0/OpenID Connectの2つのトークンの使いみち](https://qiita.com/wadahiro/items/ad36c7932c6627149873)

イメージとしては, 

1. 更新トークンを取得
2. その更新トークンを利用して, IDトークン・アクセストークンを取得
3. IDトークンとアクセストークンを利用してCognitoの認証を行う

という感じだと思っています. 更新トークンはどこかに保管しておいて, APIコールを呼び出すタイミングでIDトークンやアクセストークンの有効期限が切れていれば, トークンエンドポイントからそれらを取得して使うという感じだと思います.


|       |  更新トークン  | IDトークン   | アクセストークン|
|-------|:------------:|:----------:|:----------------:|
|設定可能な有効期限|60分～3650日|5分～1日|5分～1日|
|主な用途(？)|IDトークン・アクセストークンを取得する|APIコールなどに使用|APIコールなどに使用|

## 使用するパッケージ

* [http](https://pub.dev/packages/http)
* [amazon_cognito_identity_dart_2](https://pub.dev/packages/amazon_cognito_identity_dart_2)
* [flutter_web_auth](https://pub.dev/packages/flutter_web_auth)
* [jwt_decode](https://pub.dev/packages/jwt_decode)

いつも通り`pubspec.yaml`に以下を追記して`flutter pub get`します(バージョンは最新のものを使用してください).


```yaml:pubspec.yaml
http: ^0.12.2
amazon_cognito_identity_dart_2: ^0.1.16
flutter_web_auth: ^0.2.4
jwt_decode: ^0.1.0
```

また, この記事だけでは情報が十分でないところは多々あると思いますので, 各パッケージのAPI Referenceも参照するようにしてください.

## ユーザープール, IDプールの情報をまとめておく

毎回これらの情報を直接打ち込むのは運用・保守の観点からしてもよろしくないので, 何か1つのDartファイルにまとめて書いておきます. 自分たちの設定したものに書き換えてください.

* **`callbackScheme`には`://`を除いたものを入れていることに注意してください.** FlutterWebAuthパッケージに渡す際に`://`が含まれないものを渡す必要があるため, このようにしています.
* `cognitoUserPool`は[amazon_cognito_identity_dart_2](https://pub.dev/packages/amazon_cognito_identity_dart_2)パッケージの`CognitoUserPool`オブジェクトです. これに付随するメソッド(`.getRegion(), .getClientId()`)を利用するために定義しています.

これ以降の説明ではこの変数を使っていきます. これらを使用するDartファイルに, このDartファイルをimportしてください. このファイルを`auth.dart`と呼ぶことにします.

```dart:auth.dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

// Cognito User Pool
// 自分で決めたドメイン
// ex.) 'foo-bar123'
const cognitoDomain = 'foo-bar123';

// ユーザープールID
// ex.) 'us-east-1_XXXXXXXXX'
const cognitoUserPoolId = 'us-east-1_XXXXXXXXX';

// アプリクライアントID
// ex.) 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
const cognitoClientId = 'xxxxxxxxxxxxxxxxxxxxxxxxxx';

// コールバックURL
// "://"が含まれないことに注意
const callbackScheme = 'myapp';

// 今回はAUthorization Codeでのレスポンスが欲しいので'code'を指定
// ほかにも'token'なども指定可能
const cognitoOAuthResponseType = 'code';

// ユーザープールで設定したScopeのうちここで使いたいScopeをスペースで区切って入力
const cognitoScope = 'openid email';

// Create Cognito User Pool
// 上で設定したプールIDとアプリクライアントIDをつかって, CognitoUserPoolクラスのオブジェクトを作成.
// これ以降, このオブジェクトを使いまくります
final cognitoUserPool = CognitoUserPool(cognitoUserPoolId, cognitoClientId);

// Cognito ID Pool
// IDプールのID
// ex.) 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
const cognitoIdentityPoolId = 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';

// Lambda / API Gateway
// ex.) 'https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com'
const apiEndpoint = 'https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com';
```

## メールアドレスとパスワードで認証

### 新規登録

ユーザーが入力したメールアドレス・パスワードが`userEmail`, `userPassword`だとします. また, ユーザーが入力したレジストレーションキーが`userRegistrationKey`だとします.

1. 上(`auth.dart`)で定義した, `cognitoUserPool`に対して, `signUp()`メソッドで登録を行います. 戻り値は, `CognitoUserPoolData`型のオブジェクトであり, ここでは`cognitoUserPoolData`とします. メールアドレスに不備があったり, パスワードの要件を満たさなかったりした場合は, `CognitoClientException`が発生します. エラーなく実行できた場合は, `userEmail`のメールアドレス宛にレジストレーションキーが送信されています.
2. 上で返ってきた, `cognitoUserPoolData`の`user`プロパティは`CognitoUser`型のオブジェクトです. これに対して, `confirmRegistration()`メソッドで, レジストレーションキーの確認をすることができます. ここでも, 例外が発生した場合は, `CognitoClientException`でキャッチできます.

```dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

// Registration
try {
  CognitoUserPoolData cognitoUserPoolData = await cognitoUserPool.signUp(userEmail, userPassword);
} on CognitoClientException catch (e) {
  print(e);
}

// Confirm Registration Key
try {
  await cognitoUserPoolData.user.confirmRegistration(userRegistrationKey);
} on CognitoClientException catch (e) {
  print(e);
}
```

### ログイン

ユーザーが入力したメールアドレス・パスワードが`userEmail`, `userPassword`だとします.

1. `userEmail`と, 上(`auth.dart`)で定義した, `cognitoUserPool`から`CognitoUser`型のオブジェクト`cognitoUser`を作成します.
2. `userEmail`と`userPassword`から`AuthenticationDetails`型のオブジェクト`authenticationDetails`を作成します.
3. `cognitoUser`に対して, `authenticationDetails`を引数に渡した`authenticateUser()`メソッドでユーザーを認証できます. パスワードが違うなどの例外もキャッチできます. 戻り値は, `CognitoUserSession`型のオブジェクトであり, ここでは`cognitoUserSession`とします.
4. (optional) `cognitoUserSession`には各種トークンの情報が含まれるため, それらを取得して, `userId`を取得するのに使えます(後述の [一意なユーザーIDを取得する](#一意なユーザーidを取得する) を参照).

```dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

CognitoUser cognitoUser = CognitoUser(userEmail, cognitoUserPool);
final authenticationDetails = AuthenticationDetails(username: userEmail, password: userPassword);
try {
  final cognitoUserSession = await cognitoUser.authenticateUser(authenticationDetails);
  // 以下の3つは必要に応じて使ってください.
  final refreshToken = cognitoUserSession.getRefreshToken().token;
  final idToken = cognitoUserSession.getIdToken().jwtToken;
  final accessToken = cognitoUserSession.getAccessToken().jwtToken;
} catch (e) {
  print(e);
}
```

### パスワードを忘れた場合

ユーザーが入力したメールアドレス・パスワードが`userEmail`, `userNewPassword`だとします. また, ユーザーが入力したリセットコードが`userResetCode`だとします.

1. `userEmail`と, 上(`auth.dart`)で定義した, `cognitoUserPool`から`CognitoUser`型のオブジェクト`cognitoUser`を作成します.
2. `cognitoUser`に対して, `forgotPassword()`メソッドを実行し, `userEmail`のメールアドレス宛にリセットコードを送信します. ただ, このメソッドでは, `userEmail`がCognitoに存在するか(すでに登録されたものか)を判別していないため, 登録されていないメールアドレスを入力しても例外が発生しないことに注意してください.
3. 2が問題なく実行できた場合, `cognitoUser`に対して, `confirmPassword()`メソッドを実行して新しいパスワードを登録します. 引数は, `userResetCode`と`userNewPassword`です. ここでもパスワードが要件を満たしていないなどの例外をキャッチできます.

```dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

CognitoUser cognitoUser = CognitoUser(userEmail, cognitoUserPool);
try {
  await cognitoUser.forgotPassword();
} catch (e) {
  print(e);
}

// Create New Password
try {
  await cognitoUser.confirmPassword(userResetCode, userNewPassword);
} catch (e) {
  print(e);
}
```

### パスワードの変更

ここでは詳しく紹介しませんが, 上と同様に`cognitoUser`に対して`changePassword()`メソッドを実行すればパスワードの変更ができます.

`CognitoUser`クラスにはほかにも様々なメソッドがあるので, 是非[API Reference](https://pub.dev/documentation/amazon_cognito_identity_dart_2/latest/cognito/CognitoUser-class.html)を確認してみてください.



## サードパーティで認証(Google, LINEでログイン・新規登録)

サードパーティで認証を行う場合, ログイン・新規登録の流れは以下のようになります. ログインの場合でも新規登録の場合でも挙動は同じです.

1. 認証エンドポイントにアクセス
2. ユーザーがサードパーティのアカウントでログインすると, Authorization Codeが返ってくる.→アプリにリダイレクト
3. Authorization Codeを利用して, トークンエンドポイントから, 3種類のトークンを取得する.

### 1. 認証エンドポイント

認証エンドポイントは, 以下のようになり, Cognitoユーザープールでの設定により変わります.
アプリ内で「Googleでログイン」などのボタンをユーザーが押すと, このURLにアクセスさせ, 認証が終われば`response_type`に従ったレスポンスが返ってきます.

* `{}`の部分は上(`auth.dart`)で定義した変数に置き換えます. もちろんですが, すべて文字列として結合するので, ''は含まれません.
* `cognitoRegion`は上(`auth.dart`)で定義した変数ではありませんが, ここでは便宜上このように書いています. `final cognitoRegion = 'us-east-1'`などに定義しておいても良いのですが, 先述のように, `cognitoUserPool.getRegion()`と, `cognitoUserPool`に対して`getRegion()`メソッドを使えばとってくることができるので, わざわざ定義していません.
* `redirect_uri`はCognitoユーザープールで設定したものと正確に一致している必要があり, `callbackScheme`に`://`を書き足していることに注意してください.

> https://{cognitoDomain}.auth.{cognitoRegion}.amazoncognito.com/login?response_type={cognitoOAuthResponseType}&client_id={cognitoClientId}&scope={cognitoScope}&redirect_uri={callbackScheme}://

このままでも使えるのですが, どのサードパーティアカウントで認証したいのかをもう一度ユーザーが選ばなければならないのが不満だったので, 以下のURLに直接アクセスさせるようにしました.
`identityProvider`は, どのサードパーティアカウントで認証させるかにより変わります. 例えばGoogleであれば`Google`が入りますし, Open ID Connectの場合は, その設定の際に入力した「プロバイダー名」が`identityProvider`となります.

> https://{cognitoDomain}.auth.{cognitoRegion}.amazoncognito.com/oauth2/authorize?response_type={cognitoOAuthResponseType}&client_id={cognitoClientId}&identity_provider={identityProvider}&scope={cognitoScope}&redirect_uri={callbackScheme}://

### 2. アプリにリダイレクト

上の認証エンドポイントのURLをインターナルブラウザで開いて, レスポンスを受け取るところまでを, [flutter_web_auth](https://pub.dev/packages/flutter_web_auth)に任せます.

使い方は簡単で, アクセスさせたい`url`と`redirect_uri(callbackScheme)`を`FlutterWebAuth.authenticate()`に渡すだけです. 戻り値の型はもちろん`Future`型なので`async-await`で使います.
ユーザーがアクセスしたにも関わらず, ログインせずに戻ってきた場合は`PlatformException`を吐くので, エラーを検出するために`try-catch`文で書くと例えば以下のようになります.

```dart
try {
  final result = await FlutterWebAuth.authenticate(url: url, callbackUrlScheme: callbackScheme);
} on PlatformException catch (e) {
  print(e);
}
```

**ここでの, `callbackScheme`は上で変数に格納したように, `://`を含まないことに注意してください.**

ここで返ってくる`result`は以下のような形になります.

```
myapp://?code=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```

**これの`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`の部分がAuthorization Codeです.**
これを以下のようにparseしてとってきます.

```dart
final cognitoAuthCode = Uri.parse(result).queryParameters['code'];
```


#### Androidの場合はAndroidManifest.xmlの編集が必要

Androidでアプリにリダイレクトさせるためには追加の設定が必要です.
`android/app/src/main/AndroidManifest.xml`に以下を追記します. 上と同じですが, `myapp`の部分は設定したコールバックスキームに書き換えてください. `://`は含みません.

```xml:android/app/src/main/AndroidManifest.xml
<!-- Flutter Web Auth -->
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity">
  <intent-filter android:label="flutter_web_auth">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <!-- ここは書き換える -->
    <data android:scheme="myapp"/>
  </intent-filter>
</activity>
```

追記する場所ですが, `<manifest>`→`<application>`タグの中に書きます. デフォルトであれば, `<application>`タグの中には, 最初に`<activity android:name=".MainActivity" …`で始まる`<activity>`タグがあると思いますが, その`<activity>`タグと**並列**になるように追記します.


### 3. トークンエンドポイントからトークンを取得する

2で取得したAuthorization Codeを使ってトークンエンドポイントにhttpリクエストを送り(ここで, [http](https://pub.dev/packages/http)パッケージを使います), トークンを受け取ります. 詳細は[トークンエンドポイント - Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html)を参照してください.

トークンエンドポイントは, 以下のようになります.

> https://{cognitoDomain}.auth.{cognitoRegion}.amazoncognito.com/oauth2/token

また, ヘッダーは固定で, 
```
{'Content-Type': 'application/x-www-form-urlencoded'}
```
となっています.

あと必要な情報は, リクエストのbodyですが, ここでは, 

* Authorization Codeから更新トークン・IDトークン・アクセストークンを取得する方法
* 更新トークンからIDトークン・アクセストークンを取得する方法

を解説します.

#### Authorization Codeから更新トークン・IDトークン・アクセストークンを取得する

bodyに必要な中身は以下のようになります.

* `'grant_type'`は`'authorization_code'`で固定です. 下4つのパラメータの値は, 上(`auth.dart`)で定義した変数を使っています.
* こちらの`'redirect_uri'`もCognito ユーザープールで設定したものと正確に一致する必要があり, `'myapp://'`のように`://`が含まれるため, 下の表では`callbackScheme`と`'://'`を連結していることに注意してください.

|パラメータ|値|
|:--:|:--:|
|`'grant_type'`|`'authorization_code'`|
|`'client_id'`|`cognitoUserPool.getClientId()` (`cognitoCliendId`でも同じ)|
|`'code'`|`cognitoAuthCode`|
|`'redirect_uri'`|`callbackScheme + '://'`|
|`'scope'`|`cognitoScope`|

全体としてまとめて, httpリクエストを送ってresponseを受け取るところまでを書くと, 以下のようになります. `response`は`http.Response`型です.

```dart
import 'package:http/http.dart' as http;

// tokenEndPointのURLを作成
final tokenEndpoint = Uri.https(cognitoDomain + '.auth.' + cognitoUserPool.getRegion() + '.amazoncognito.com', '/oauth2/token');

http.Response response;

try {
  response = await http.post(
    tokenEndpoint,
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: {
      'grant_type': 'authorization_code',
      'client_id': cognitoUserPool.getClientId(),
      'code': cognitoAuthCode,
      'redirect_uri': callbackScheme + '://',
      'scope': cognitoScope,
    },
  );
} catch (e) {
  print(e);
}
```

この`response`から, 各種トークンを取得するには以下のように, `dart:convert`に含まれる`json.decode()`を利用して(importが必要です),

```dart
import 'dart:convert';

final refreshToken = json.decode(response.body)['refresh_token'];
final idToken = json.decode(response.body)['id_token'];
final accessToken = json.decode(response.body)['access_token'];
```

となります.


#### 更新トークンからIDトークン・アクセストークンを取得する

bodyに必要な中身は以下のようになります.

* `'grant_type'`は`'refresh_token'`で固定です. `'client_id'`は上(`auth.dart`)で定義した変数を使っています.
* `'refresh_token'`は, これ以前にどこかで(例えば上のAuthorization Codeなどから)取得したものを指定します. ここでは, `refreshToken`という変数に格納されているとします.

|パラメータ|値|
|:--:|:--:|
|`'grant_type'`|`'refresh_token'`|
|`'client_id'`|`cognitoUserPool.getClientId()` (`cognitoCliendId`でも同じ)|
|`'refresh_token'`|`refreshToken`|

全体としてまとめて, httpリクエストを送ってresponseを受け取るところまでを書くと, 以下のようになります. `response`は`http.Response`型です.

```dart
import 'package:http/http.dart' as http;

// tokenEndPointのURLを作成
final tokenEndpoint = Uri.https(cognitoDomain + '.auth.' + cognitoUserPool.getRegion() + '.amazoncognito.com', '/oauth2/token');

http.Response response;

try {
  response = await http.post(
    tokenEndpoint,
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: {
      'grant_type': 'refresh_token',
      'client_id': cognitoUserPool.getClientId(),
      'refresh_token': refreshToken,
    },
  );
} catch (e) {
  print(e);
}
```

この`response`から, 各種トークンを取得するには以下のように, `dart:convert`に含まれる`json.decode()`を利用して(importが必要です),

```dart
import 'dart:convert';

final idToken = json.decode(response.body)['id_token'];
final accessToken = json.decode(response.body)['access_token'];
```

となります.

## 一意なユーザーIDを取得する

ここでは, ユーザーを識別するために必要な一意なユーザーIDを取得する方法を解説します. 以下の2つの方法を見ていきます.

* 取得したIDトークンをデコードして, ユーザープールでいう`sub`を取得する
* 取得した更新トークンを利用して, IDプールのユーザーIDを取得する

**上記2つのIDは別物であるため, どちらを使うかはアプリ内で統一してください.**

#### IDトークンをデコードする

取得したIDトークンは, 単純にエンコードされたものなので, デコードすることで中身を知ることができます. デコードに, [jwt_decode](https://pub.dev/packages/jwt_decode)パッケージを使用します.

ここでは, `idToken`に取得したIDトークンが格納されているとします.
1. `idToken`をamazon_cognito_identity_dart_2の`CognitoIdToken`オブジェクトにします.
2. `getJwtToken()`メソッドで`JwtToken`に変換します. 
3. それを`Jwt.parseJwt()`でデコードします.
4. `payload`は`Map`で, 今回はその中の`sub`を取得する.

```dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';
import 'package:jwt_decode/jwt_decode.dart';

final cognitoIdToken = CognitoIdToken(idToken);
final payload = Jwt.parseJwt(cognitoIdToken.getJwtToken());
final userId = payload['sub']
```

これで, 一意なユーザーID(`userId`)を取得することができました.

ちなみに, `payload`には`sub`だけでなく, 例えば`email`など, ユーザーのほかの情報も含まれるため, 同様にしてそれらとってくることもできます.

参考(再掲) : [Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する](https://dev.classmethod.jp/articles/study-tokens-of-cognito-user-pools/)


#### IDプールのユーザーIDを取得する

先述の`sub`でも構いませんが, こちらのIDもユーザーごとに一意なのでユーザーを識別するためのIDとして利用できます

ここでは, `idToken`に取得したIDトークンが格納されているとします.
1. `idToken`を`CognitoIdToken`オブジェクトにします.
2. `CognitoCredentials`オブジェクトを作成します. ここでの引数は, 上(`auth.dart`)で定義した`String`型の`cognitoIdentityPoolId`と, `CognitoUserPool`オブジェクトの`cognitoUserPool`です.
3. `cognitoCredentials.getAwsCredentials()`に`JwtToken`を渡して, `AwsCredentials`を取得します.
4. `cognitoCredentials`の`userIdentityId`プロパティがIDプールのユーザーIDです.

```dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

final cognitoIdToken = CognitoIdToken(idToken);
CognitoCredentials cognitoCredentials = CognitoCredentials(cognitoIdentityPoolId, cognitoUserPool);
await cognitoCredentials.getAwsCredentials(cognitoIdToken.getJwtToken());
final userId = cognitoCredentials.userIdentityId;
```

これで, 一意なユーザーID(`userId`)を取得することができました.

## (おまけ) APIコールについて

この記事はAPIに関することがメインではないので, ここでは実際にLambda関数を作ったりするようなことはしません. また, 前提としてLambdaとAPI Gatewayはつながっているとします.

### AWS側の設定

AWS側で少し設定が必要です.

#### 1. オーソライザーを作成する

1. 下のように, 左のメニューからオーソライザーを選び, 「新しいオーソライザーの作成」をクリックします.

![](https://i.imgur.com/18wlPNE.png)

2. 名前は適当につけ, タイプは「Cognito」にし, 上で作成したCognitoユーザプールを選びます(リージョンを選んで右のフィールドをクリックすればユーザープール一覧が出てきます. 出てこない場合はリージョンを確認してください). トークンのソースも適当なものを入力してください. 終わったら, 「作成」をクリックしてください.


![](https://i.imgur.com/8yHyBUu.png)

#### 2. リソースに認可を与える

1. 左のメニューから「リソース」を選び, 認可を与えたいリソースのメソッドを選択してください.

2. 「メソッドリクエスト」をクリックしてください.

![](https://i.imgur.com/qon8bbi.png)

3.「**認可** なし」となっているところの横のペンマークをクリックして, ドロップダウンから先ほど作成したオーソライザーを選択してください. 出てこない場合は, ページを再読み込みしてください. 選択できたら, 横のチェックマークをクリックしてください。

![](https://i.imgur.com/C69RVJX.png)

### Flutter側からhttpリクエストを送る

`idToken`, `accessToken`は上で取得したものです. また, `apiEndpoint`は`auth.dart`で定義した変数です.
APIによっても様々なので一例として

* リソースパスが`/testapi`というAPI Gateway内のリソースに対して
* `method`が`POST`
* `headers`が`requestHeaders`
* `body`が`requestBody`

のリクエストを送る場合を書いています. `response`がもちろんレスポンスです. ちなみに, リソースパスは, 以下の部分で確認できます.

![](https://i.imgur.com/8VqLTJb.png)


```dart
import 'package:amazon_cognito_identity_dart_2/cognito.dart';

CognitoUserSession cognitoUserSession = CognitoUserSession(idToken, accessToken);

final cognitoCredentials = CognitoCredentials(cognitoIdentityPoolId, cognitoUserPool);
await cognitoCredentials.getAwsCredentials(cognitoUserSession.getIdToken().getJwtToken());

final awsSigV4Client = AwsSigV4Client(cognitoCredentials.accessKeyId, cognitoCredentials.secretAccessKey, apiEndpoint, sessionToken: cognitoCredentials.sessionToken, region: cognitoUserPool.getRegion());

final signedRequest = SigV4Request(awsSigV4Client, method: 'POST', path: '/testapi', headers: requestHeaders, body: requestBody);

http.Response response;
try {
  response = await http.post(
    signedRequest.url,
    headers: signedRequest.headers,
    body: signedRequest.body,
  );
} catch (e) {
  print(e);
}
```

## ログイン状態の保持について

ここまで見てきたように, 例えばユーザーのパスワード変更や, APIコールなどにはIDトークン・アクセストークンが必要でした. しかし, 「[更新トークン・IDトークン・アクセストークン](#更新トークンidトークンアクセストークン)」のところで見たように, IDトークン・アクセストークンの有効期限は長くても1日です. つまり, このままでは, ユーザーは1日ごとにログインしなくてはならなく, とても不便です.

そこで, 有効期限が長く設定できる更新トークンに着目します. 「[更新トークンからIDトークン・アクセストークンを取得する](#更新トークンからidトークンアクセストークンを取得する)」で見たように, 更新トークンさえあればIDなど必要なものをすべて取得できます.

ただし, もちろん更新トークンにも有効期限がありますから, その有効期限が切れた際はログアウト状態となり, [再度ログイン](#サードパーティで認証google-lineでログイン新規登録)が必要となります. つまり, この方法を取れば, ユーザーがログイン操作をする必要のある頻度は, 更新トークンの有効期限の長さに反比例するようにできます.

実装方法ですが, 例えば, [shared_preferences](https://pub.dev/packages/shared_preferences)というパッケージを使い, 更新トークンをSharedPreferencesに保存するという方法があります.

他には, [amazon_cognito_identity_dart_2](https://pub.dev/packages/amazon_cognito_identity_dart_2)の`CognitoUser`クラスには`cacheTokens()`メソッドや, `clearCachedTokens()`メソッドがあるので, これらを使うことができるかもしれません(やったことはないです).