インドカレーファンクラブ

パソコン、カメラ

【React】【Cognito】AWS CognitoのSMS MFA認証を試す(割と親切記事)

あらすじ

表題の件を試すには正直言ってAWS Amplifyあたりを使うのが賢いと思う

でもいきなり便利なものを使って泥臭い方法を知らないまま進むと大概苦しむことになるわけで、それはシステムが僕の首に描いた数々の索条痕が証明している

地道にやる

やることの概要

  • バックエンド(AWS)をTerraformでつくる
  • Cognitoにユーザを追加する
    • ユーザ追加はAWS Consoleでやる(極力サボりたいので)
    • ユーザ初回認証(PW再設定)はAWS CLIでやる(極力サボりたいので)
  • フロントをReactでつくる
    • ID:PW入れたらSMSが飛んできて、そのPINを入力すると認証通る!というとこだけ
    • ユーザ作成とかPW再設定とかはサボる
  • いざSMS MFA

バックエンド(AWS)の構築

AWS Cognitoを使う

今回必要なリソースはTerraformでつくった

gistに投げてあるので参考に

sample terraform file for creating AWS Cognito (MFA) · GitHub

sms_role_ext_idとかパスワードポリシーとかセッションタイムアウトとか、自分のものに変えるように

これで以下の通りリソースを作る

  • MFAでSMSを送るIAMロール
  • CognitoのUserPool
  • CognitoのUserPoolのクライアント
  • CognitoのIdentityPool
  • 認証がOKな場合のIAMロール※
  • 認証がNGな場合のIAMロール※
  • CognitoのIdentityPoolと認証OK/NG用IAMロールの紐付け

ここで作ったリソースのIDやらをフロント側で利用することになる

Cognitoにユーザを追加

追加

・ユーザ追加はAWS Consoleでやる(極力サボりたいので)

f:id:omdwn:20210728174458p:plain

Terraformで構築したユーザプールにユーザを作る
その際に特に気をつけたいこと: ユーザが日本にいるならその電話番号は国コード+81を頭につけておかないとダメ
例えば090-1234-5678ならば+819012345678

初期パスワードはパスワードポリシーに沿ったもの

メールアドレスは今回使わないんだけど、Cognitoの構築時にメール必須にしちゃったから入れた(うっかり)

この状態だとアカウントのステータスがFORCE_CHANGE_PASSWORDのままなので認証を通す必要がある

認証

初回認証

・ユーザ初回認証(PW再設定)はAWS CLIでやる(極力サボりたいので)

認証というか、よくある「本人確認のついでに初期パスワードを変更してください」というやつの対応をする
今回は初期パスワードをもう一回入力して使い回す

AWS CLIが使える環境でこんな感じのことをすればOK

# 変数にUSERNAME使ったリするとOSの環境変数とぶつかったりして怖い気がする
AUTH_USERNAME='test-user'
USER_POOL_ID='ap-northeast-XXXXXXXXXXX'
CLIENT_ID='XXXXXXXXXXXXXXXXXXXXXXXXXX'
AUTH_PASSWORD='XXXXXXXXXX'

aws cognito-idp admin-initiate-auth \
--user-pool-id ${USER_POOL_ID} \
--client-id ${CLIENT_ID} \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters "USERNAME=${AUTH_USERNAME},PASSWORD=${AUTH_PASSWORD}" \
> result-admin-initiate-auth.txt

# これどっかの記事を参考にしたと思うんだけど見つからなかった...
SESSION_ID=`cat result-admin-initiate-auth.txt | grep Session | cut -d ":" -f 2 | tr -d '"' | tr -d ' ' | sed '$s/.$//' `

aws cognito-idp admin-respond-to-auth-challenge \
--user-pool-id ${USER_POOL_ID} \
--client-id ${CLIENT_ID} \
--challenge-name NEW_PASSWORD_REQUIRED \
--challenge-responses "USERNAME=${AUTH_USERNAME},NEW_PASSWORD=${AUTH_PASSWORD}" \
--session ${SESSION_ID}

参考:プログラミングせずにCognitoで新規ユーザー登録&サインインを試してみる | DevelopersIO

この時点でアカウントのステータスはCONFIRMEDに変化し、ついでに指定した電話番号に「認証コードは ****** です」というSMSが飛んでくる
が今回の目的は初回認証だったのでこれは無視

因みにこれ+この続きの中で出てくる --auth-flow ADMIN_USER_PASSWORD_AUTH という形で指定する認証方式が重要で、ここで指定したものはCognito側の設定で許可しておく必要がある

折角なのでCLIでSMS認証

上記のスクリプトとほぼ同じだけどちょっと違うので注意

AUTH_USERNAME='test-user'
USER_POOL_ID='ap-northeast-XXXXXXXXXXX'
CLIENT_ID='XXXXXXXXXXXXXXXXXXXXXXXXXX'
AUTH_PASSWORD='XXXXXXXXXX'
# admin-initiate-authじゃなくてinitiate-auth
aws cognito-idp initiate-auth \
 --auth-flow USER_PASSWORD_AUTH \
 --client-id ${CLIENT_ID} \
 --auth-parameters "USERNAME=${AUTH_USERNAME},PASSWORD=${AUTH_PASSWORD}" \
> result-admin-initiate-auth.txt

でSMSが送られてくるので...

# PINに差し替え
SMS_MFA_CODE='XXXXXX'

SESSION_ID=`cat result-admin-initiate-auth.txt | grep Session | cut -d ":" -f 2 | tr -d '"' | tr -d ' ' | sed '$s/.$//' `

# admin-respond-to-auth-challengeじゃなくてrespond-to-auth-challenge
aws cognito-idp respond-to-auth-challenge \
 --client-id ${CLIENT_ID} \
 --challenge-name SMS_MFA \ # MFA!
 --challenge-responses \
 USERNAME=${AUTH_USERNAME},SMS_MFA_CODE=${SMS_MFA_CODE} \
 --session ${SESSION_ID}

成功するとこんな感じのレスポンスが返ってくる

{
    "ChallengeParameters": {},
    "AuthenticationResult": {
        "AccessToken": "xxxxx",
        "ExpiresIn": 300,
        "TokenType": "Bearer",
        "RefreshToken": "xxxxx",
        "IdToken": "xxxxx"
    }
}

ここのIdTokenがよくいうjwt tokenというもの

他に何も変えてないけど色々試してたら急にSMS送られなくなったんだけど?!という場合、クォータの問題だったりするかも

omdwn.hatenablog.com

フロントの構築

Reactを使う

create-react-appを使って足場をつくり、スタイリングはbulmaでガサツにやる

github.com

実現していることは

・ID:PW入れたらSMSが飛んできて、そのPINを入力すると認証通る!というとこだけ
・ユーザ作成とかPW再設定とかはサボる

これだけ

具体的にやっていることはReadMeとかコード見たほうが早いかも
React周辺で至らぬところがありそうでごめんなさい

動き的には、認証が必要なページ(/)があって、そこへのアクセス時に認証が済んでなければ(/login)に飛ばしてログインさせる感じ

f:id:omdwn:20210728175018p:plain

認証後にはjwt_tokenというのを表示するとこまでやっている

この辺参考になるかも:
Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する | DevelopersIO

Cognitoを使う場合、最終的にはこのトークンを用いてAPI Gateway(AuthorizerにCognito)に認証付きのリクエストを飛ばすことになるはず
でも今回はAPI Gatewayのリソースつくるの面倒くさいから端折った

import useSWR from 'swr'

// ...

async function fetcher(url: string, jwt_token: string) {
  // ここでトークンを指定!
  const res = await fetch(url, { headers: { Authorization: jwt_token } });
  const json = await res.json();
  return json;
}

// どっかから拾ってくる
const jwt_token = token;

const API_URL = 'YOUR API GATEWAY ENDPOINT';
const { data, error } = useSWR([API_URL, jwt_token], fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
const result = JSON.parse(data.body);
// resultをあれこれ...

あとデザインはここのを参考にさせていただいた

おわりに

大変だった

参考にしたもの

全般

www.tdi.co.jp

こちらはjQueryで実現している
今回Reactを使いたかった(というよりjQueryを使いたくなかった...)からコードはそこまで参考にしていないけど、それ以外のCognitoにまつわる全体的な話は超参考にさせていただいた

qiita.com

こっちは素Nodeでコードをだいぶ参考にさせていただいた

Cognito + API Gateway

Amazon Cognito と仲良くなるために歴史と機能を整理したし、 Cognito User Pools と API Gateway の連携も試した | DevelopersIO

Amazon API Gateway をクロスオリジンで呼び出す (CORS) | DevelopersIO

Amazon API GatewayでAPIキー認証を設定する | DevelopersIO

API GatewayのオーソライザーにAmazon Cognitoを使ってみた件 - サーバーワークスエンジニアブログ

Terraform

【AWS】TerraformでCognitoをつくって楽したいんだ! | Katsuya Place

フロントのコードの参考

node.js - 'AWSCognito' is not defined - Stack Overflow

Reactでroot importする方法 - React(リアクト)でコンポーネント(Component)を呼ぶ時(import)、rootフォルダーを基準にして参照できるように設定して見ます。

amazon-cognito-identity-jsがセッション情報をsessionStorageに保存できるようになった - Qiita

create-react-appで独自の環境変数を読み込む - Qiita

SWRを使おうぜという話