【React】【Cognito】AWS CognitoのSMS MFA認証を試す(割と親切記事)
あらすじ
表題の件を試すには正直言ってAWS Amplifyあたりを使うのが賢いと思う
でもいきなり便利なものを使って泥臭い方法を知らないまま進むと大概苦しむことになるわけで、それはシステムが僕の首に描いた数々の索条痕が証明している
地道にやる
やることの概要
- バックエンド(AWS)をTerraformでつくる
- Cognitoにユーザを追加する
- フロントを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でやる(極力サボりたいので)
Terraformで構築したユーザプールにユーザを作る
その際に特に気をつけたいこと: ユーザが日本にいるならその電話番号は国コード+81を頭につけておかないとダメ
例えば090-1234-5678ならば+819012345678
初期パスワードはパスワードポリシーに沿ったもの
メールアドレスは今回使わないんだけど、Cognitoの構築時にメール必須にしちゃったから入れた(うっかり)
この状態だとアカウントのステータスがFORCE_CHANGE_PASSWORDのままなので認証を通す必要がある
認証
初回認証
認証というか、よくある「本人確認のついでに初期パスワードを変更してください」というやつの対応をする
今回は初期パスワードをもう一回入力して使い回す
# 変数に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送られなくなったんだけど?!という場合、クォータの問題だったりするかも
フロントの構築
Reactを使う
create-react-appを使って足場をつくり、スタイリングはbulmaでガサツにやる
実現していることは
・ID:PW入れたらSMSが飛んできて、そのPINを入力すると認証通る!というとこだけ
・ユーザ作成とかPW再設定とかはサボる
これだけ
具体的にやっていることはReadMeとかコード見たほうが早いかも
React周辺で至らぬところがありそうでごめんなさい
動き的には、認証が必要なページ(/
)があって、そこへのアクセス時に認証が済んでなければ(/login
)に飛ばしてログインさせる感じ
認証後には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をあれこれ...
あとデザインはここのを参考にさせていただいた
おわりに
大変だった
参考にしたもの
全般
こちらはjQueryで実現している
今回Reactを使いたかった(というよりjQueryを使いたくなかった...)からコードはそこまで参考にしていないけど、それ以外のCognitoにまつわる全体的な話は超参考にさせていただいた
こっちは素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