💡

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

2022/12/08に公開

記載内容

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