💡

【Flutter × AWS without Amplify】Cognitoでログイン〜認可情報を使ってAPIを叩くまで

2022/12/08に公開約4,600字

記載内容

Flutter × Cognito を利用した認証・認可の設定まで(API叩けるようになるまで)の解説
パッケージ:amazon_cognito_identity_dart_2 ※非公式

APIに接続するにはCognito側での認可情報が必要とする

※セッション情報の保持については扱いません。
 ⇒アプリならShared Preferences、WebならCookiesクラスでいけたと思います。
  詳しくはまた別の記事にしようかと
※API Gatewayの設定については扱いません。

(前段)なぜAmplifyを使わないのか?

1.制約が見えない
Firebaseも同じことが言えますが、AmplifyはAWSの各サービスを抽象化してパッケージングしたサービスです。パッケージングされた時に元のサービスの挙動や仕様がそのままであればいいのですが「実はこういう制約がありました」なんてことがあると厄介なので、手が出しづらいと思っています。

特に私自身がAWSに馴染みがあるわけではないので、そういうリスクはなるべく避けていきたいなという思いから、Amplifyを使わないという判断に至りました。

2.自身の学習・スキルアップのため
Amplifyで構築するとさぞかし楽なんだと思います。
実際AWSではないですが、Firebase × Flutterで個人開発をしていたときはすごく楽に感じました。インフラ周りの考慮事項が少ない(というかほぼ無い)ですし、特にFirestoreに至ってはアプリクライアントとFirestoreが直接接続するようにCRUD処理を行うので、通信経路すら考えなくても済みました。

しかし、そうやって考慮事項を後回しにしていると”ツケが回る”わけでして、知識のある方々のお話についていけなかったり、少し抽象度を下げてカスタマイズしようとすると途端に知識のハードルが上がってついていけなくなったりと大変で、到底実務レベルとは言えないと思いました。そんなことから、Amplifyなしで構築していくことで、
AWSのサービスそのものや、AWSを使う前提として必要な知識について棚卸し・キャッチアップをしていこうと思いました。

ざっくり手順

1.前提となる知識を理解する("理解するまでの壁"を参照)
2.Amazon Cognitoの公式ドキュメントを読む
3.実装

理解するまでの壁

HTTPプロトコルについての理解
トークン認証についての理解
  • クレデンシャル(Credentials)
  • OAth 2.0
  • OIDC
  • アクセストークン
  • リフレッシュトークン
  • IDトークン
  • JWTトークン
    • ヘッダー
    • ペイロード
    • 署名



参考:
https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06
https://www.youtube.com/watch?v=PKPj_MmLq5E&ab_channel=Authlete

AWSの用語の理解
  • 署名バージョン4

  • (Cognito直下の)ユーザープール

    • ユーザープール名
    • ユーザープールID
    • クライアントID
    • ユーザー名
    • ユーザーID(Sub)
    • グループ
    • グループ名
  • フェデレーティッドアイデンティティ

    • IDプール
    • ユーザープールグループ
    • IDプールのID
    • アイデンティティID

余談
「Cognitoで認可情報を取得できたユーザーのみAPIを叩ける」を実現するためには、
フェデレーティッドアイデンティティの設定が必要です。
私は公式ドキュメントの読込が甘かったため、ここで躓きました。。。

https://www.youtube.com/watch?v=n4hsWVXCuVI&t=804s&ab_channel=AmazonWebServices


実装の流れ

1.Cognitoでユーザープールや、テスト用アカウントのセットアップ
※このときフェデレーティッドアイデンティティも設定してください
2.セットアップした内容(ユーザープール名やユーザープールIDといった固定値)を定義
3.パッケージで提供されているCognitoのオブジェクトに格納
4.セッションの確立
5.セッション情報からトークンとクレデンシャルの取得
6.署名バージョン4の設定
7.APIを叩く

実装

const String email = 'ログイン用のメールアドレス';
const String password = 'ログイン用のパスワード';


// ユーザープール名
const String cognitoDomain = 'ユーザープール名';
// ユーザープールID
const String cognitoUserPoolId = 'ap-northeast-1_XXXXXXXXX';
// クライアントID
const String cognitoClientId = 'XXXXXXXXXXXXXXXXXXXXXXXXXX';
// IDプールのID
const String cognitoFedIdPoolId =
    'ap-southeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    
// ユーザープールオブジェクトの設定
final CognitoUserPool cognitoUserPool = CognitoUserPool(
  cognitoUserPoolId,
  cognitoClientId,
);
// クレデンシャル
final CognitoCredentials credentials = CognitoCredentials(
  cognitoFedIdPoolId,
  cognitoUserPool,
);


// 認証情報の設定
final CognitoUser cognitoUser =
  CognitoUser(email, cognitoUserPool);
final AuthenticationDetails authDetails = AuthenticationDetails(
  username: email,
  password: password,
);

// セッション情報を格納するオブジェクトを定義
CognitoUserSession? session;
// ログイン処理、セッション確立
session = await cognitoUser.authenticateUser(authDetails);

// トークンの発行getJwtToken() ⇒ クレデンシャルの取得getAwsCredentials()
await credentials.getAwsCredentials(session!.getIdToken().getJwtToken());

// 署名バージョン4の設定
final AwsSigV4Client awsSigV4Client = AwsSigV4Client(
    credentials.accessKeyId!,
    credentials.secretAccessKey!,
  // APIのエンドポイント
    'https://xxxx.execute-api.ap-southeast-1.amazonaws.com/dev',
    sessionToken: credentials.sessionToken,
    region: 'リージョンが入ります',
);

// 署名バージョン4を使ったリクエスト用のオブジェクトを設定
final SigV4Request sigV4Request = SigV4Request(
    awsSigV4Client,
    method: 'GET',
    path: '',
   headers: Map<String, String>.from({'header-1': 'one', 'header-2': 'two'}),
  queryParams: Map<String, String>.from({'tracking': 'x123'}),
  body: Map<String, dynamic>.from({'color': 'blue'}),
);

// HTTPレスポンスを受け取るオブジェクトを定義
http.Response response;

// APIを叩く
try {
  response = await http.get(
    Uri.parse(sigV4Request.url!)
    // headersにAuthorization項目を設定、JWTトークンを入れる
    // パッケージのUse caseだと後述のbodyと同じようにsigV4Request.headers
    // として書かれていますが、型のエラーが出たため変更しています
    headers: Map<String, String>.from({
      'Authorization': session!.getIdToken().getJwtToken(),
      'header-2': 'two',
    }),
    // http.postなどでボディ部分を設定する必要がある場合
    // body: sigV4Request.body,
    );
} catch (e) {
  print(e);
}


Discussion

ログインするとコメントできます