AWS Cognito UserPoolをサーバーサイドで使うサンプル (Node.js)
Cognitoが全然分からなくて、クライアント側のJavaScriptで使う記事ばかり読んでしまっていた。
aws-amplify
とかamazon-cognito-identity-js
でめちゃくちゃ悩んだのに、サーバー側なら普通にaws-sdk
を使えばよかったのだった。
前提: Cognitoユーザープールの設定
この記事では、以下の通りに作成したユーザープールとアプリクライアントを使います。
- サインインはユーザー名で行う
- 必須の標準属性は無し (メアドも無し)
- ユーザーに自己サインアップを許可しない
- 一時パスワードの有効期限は1日 (サーバー側で即座に更新するので何日でもいい)
- 属性は検証しない (メアドも電話番号も無いので)
- アプリクライアントの『クライアントシークレットを生成』ON(プール作成時しか設定できないので注意)
- アプリクライアントの『サーバーベースの認証でサインインAPIを有効にする』ON
ユーザーに自己サインアップを許可せず、メアド等の検証も行わないので、ケースとしては完全に裏方としてCognitoを使う場合になると思います。ユーザー名とパスワードには、ユーザーが入力した値を使用します。
なお、メアド等の属性を検証するユーザープールだとステータス遷移が異なるっぽいので、この記事の方法でできるかは分かりません。adminConfirmSignUp()
を使えば検証もサーバーサイドで済ませられるので、そこらへんを使うのかも。
必要なものインストール
サーバー側のアプリにaws-sdk
をインストールします。
npm install --save aws-sdk
ユーザー登録 (サインアップ)
ユーザーに自己サインアップを許可しない場合、ユーザー登録にはadminCreateUser()
を使います。
ユーザーを作成した後、即座にadminSetUserPassword()
で一時パスワードを変更すれば、すぐにユーザーを使える状態になります。
const AWS = require('aws-sdk');AWS.config.update({region: 'ap-northeast-1',}); const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});(async() => {const userPoolId = 'ap-northeast-1_xxxxxxxxx';const username = 'kiriukun';const password = 'mypassword';try {// ユーザー登録// パスワード未指定の場合は自動でランダムな一時パスワードが設定されるconst user = await cognito.adminCreateUser({UserPoolId: userPoolId,Username: username,}).promise();console.log('登録完了', JSON.stringify(user, null, 4));// 作成したばかりのユーザーはステータス FORCE_CHANGE_PASSWORD なのでパスワード変更// べつに一時パスワードと同じパスワードでもエラーにはならないawait cognito.adminSetUserPassword({UserPoolId: userPoolId,Username: username,Password: password,Permanent: true}).promise();console.log('パスワード変更完了');}catch (err) {console.log(err);if (err.code == 'UsernameExistsException') {// ユーザーがすでに存在する場合} else if (err.code == 'InvalidPasswordException') {// パスワードがポリシーを満たしてない場合} else {// その他のエラー}}})();
adminCreateUser()
のレスポンスは以下のような感じでした。
{ "User": { "Username": "my_username", "Attributes": [ { "Name": "sub", "Value": "aed7ad78-f440-47b3-ad9d-aaaaaaaaaaaa" } ], "UserCreateDate": "2019-10-23T05:40:01.580Z", "UserLastModifiedDate": "2019-10-23T05:40:01.580Z", "Enabled": true, "UserStatus": "FORCE_CHANGE_PASSWORD" }}
サインイン
サインインにはadminInitiateAuth()
を使います。
ここではアプリクライアントシークレットを生成してるので、このメソッドはシークレットハッシュをパラメータに指定して呼び出す必要があります。
const crypto = require('crypto');const AWS = require('aws-sdk');AWS.config.update({region: 'ap-northeast-1',}); const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});(async() => {const userPoolId = 'ap-northeast-1_xxxxxxxxx';const clientId = 'xxxxxxxxxxxxxxxxxxxxxxxxxx';const clientSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';const username = 'kiriukun';const password = 'mypassword';try {// シークレットハッシュ計算const secretHash = crypto.createHmac('sha256', clientSecret).update(username + clientId).digest('base64');// サインインconst user = await cognito.adminInitiateAuth({UserPoolId: userPoolId,ClientId: clientId,AuthFlow: 'ADMIN_NO_SRP_AUTH',AuthParameters: {USERNAME: username,PASSWORD: password,SECRET_HASH: secretHash}}).promise();console.log('サインイン完了', JSON.stringify(user, null, 4));}catch (err) {console.log(err);if (err.code == 'UserNotFoundException') {// ユーザーが存在しない場合} else if (err.code == 'NotAuthorizedException') {// パスワードが間違ってる場合} else {// その他のエラー}}})();
この時のadminInitiateAuth()
のレスポンスは以下のような感じでした。
{ "ChallengeParameters": {}, "AuthenticationResult": { "AccessToken": "eyJraWQiOiI3eWZRR1wvZnVTQ1F4UHNGK090R1RPUnQxWUY5Tk(省略)", "ExpiresIn": 3600, "TokenType": "Bearer", "RefreshToken": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiU(省略)", "IdToken": "eyJraWQiOiJ2bURHOWVpb1dHOHFZcGs1bEFFbTZmZjRNOGYya0R3Tk(省略)" }}
トークンのリフレッシュ
IDトークン・アクセストークンのリフレッシュは、サインインと同様にadminInitiateAuth()
を使います。
必要なのはリフレッシュトークンだけで、ユーザー名とパスワードは要りません。
const crypto = require('crypto');const AWS = require('aws-sdk');AWS.config.update({region: 'ap-northeast-1',}); const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});(async() => {const userPoolId = 'ap-northeast-1_xxxxxxxxx';const clientId = 'xxxxxxxxxxxxxxxxxxxxxxxxxx';const clientSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';const refreshToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(省略)';try {// トークンリフレッシュconst res = await cognito.adminInitiateAuth({UserPoolId: userPoolId,ClientId: clientId,AuthFlow: 'REFRESH_TOKEN_AUTH',AuthParameters: {REFRESH_TOKEN: refreshToken,SECRET_HASH: secretHash}}).promise();console.log('トークンリフレッシュ完了', JSON.stringify(res, null, 4));}catch (err) {console.log(err);if (err.code == 'UserNotFoundException') {// ユーザーが存在しない場合} else if (err.code == 'NotAuthorizedException') {// パスワードが間違ってる、またはリフレッシュトークンの期限が切れてる場合} else {// その他のエラー}}})();
この時のadminInitiateAuth()
のレスポンスは以下のような感じでした。サインインの時と異なりRefreshToken
が入ってないです。
{ "ChallengeParameters": {}, "AuthenticationResult": { "AccessToken": "eyJraWQiOiI3eWZRR1wvZnVTQ1F4UHNGK090R1RPUnQxWUY5Tk(省略)", "ExpiresIn": 3600, "TokenType": "Bearer", "IdToken": "eyJraWQiOiJ2bURHOWVpb1dHOHFZcGs1bEFFbTZmZjRNOGYya0R3Tk(省略)" }}
サインアウト
サインアウトにはadminUserGlobalSignOut()
を使います。
const AWS = require('aws-sdk');AWS.config.update({region: 'ap-northeast-1',}); const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});(async() => {const userPoolId = 'ap-northeast-1_xxxxxxxxx';const username = 'kiriukun';try {// サインアウトawait cognito.adminUserGlobalSignOut({UserPoolId: userPoolId,Username: username,}).promise();console.log('サインアウト完了');}catch (err) {console.log(err);}})();
注意点ですが、サインアウトで無効になるのはアクセストークンとリフレッシュトークンだけです。IDトークン (API Gatewayでオーソライザーに使えるやつ) は無効になりません。 試しにサインアウト後にIDトークンでAPI Gatewayを叩いてみれば分かります。
なんでだろと一瞬思いましたが、そもそもIDトークン自体が有効期限の情報を含んだJSON Web Tokenなので、考えてみればそういうものかもしれません。IDトークンの検証なら、API Gatewayのオーソライザーに限らずユーザーが手動でもできるわけですから。
なのでIDトークンを即座に無効にしたい場合、アプリ側でユーザーがログアウト済みかどうかを別途管理する必要があります。
ユーザー取得
ユーザーの取得にはadminGetUser()
を使います。
const AWS = require('aws-sdk');AWS.config.update({region: 'ap-northeast-1',}); const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});(async() => {const userPoolId = 'ap-northeast-1_xxxxxxxxx';const username = 'kiriukun';try {const user = await cognito.adminGetUser({UserPoolId: userPoolId,Username: username,}).promise();console.log('取得完了', JSON.stringify(user, null, 4));}catch (err) {if (err.code == 'UserNotFoundException') {// ユーザーが存在しない場合} else {// その他のエラー}}})();
レスポンスの例は以下です。ユーザー登録時の戻りと微妙に違うので注意です (Attributes
がUserAttributes
になってる)。
{ "Username": "my_username", "UserAttributes": [ { "Name": "sub", "Value": "825d8d71-f582-4d9a-a35f-aaaaaaaaaaaa" } ], "UserCreateDate": "2019-11-22T16:11:17.490Z", "UserLastModifiedDate": "2019-11-22T16:11:17.705Z", "Enabled": true, "UserStatus": "CONFIRMED"}
ユーザー削除
ユーザーの削除にはadminDeleteUser()
を使います。
const AWS = require('aws-sdk');AWS.config.update({region: 'ap-northeast-1',}); const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});(async() => {const userPoolId = 'ap-northeast-1_xxxxxxxxx';const username = 'kiriukun';try {// ユーザー削除await cognito.adminDeleteUser({UserPoolId: userPoolId,Username: username,}).promise();console.log('削除完了');}catch (err) {console.log(err);if (err.code == 'UserNotFoundException') {// ユーザーが存在しない場合} else {// その他のエラー}}})();
以上
他にも、ここに書いてないメソッド使ったら都度追記します。
- AWS サーバーレスなSPAを単一ドメインで構築する (CloudFront, S3, API Gateway) (
2025-02-23 ) - 自宅サーバーのグローバルIPが変わったらRoute 53のAレコードを自動更新する (
2024-04-27 ) - Route53 別のAWSアカウントにサブドメイン用のホストゾーンを作成する (
2023-10-20 ) - AWS CDKで生成するテンプレートからMetadataとCDKMetadataとBootstrapVersionを除去 (
2023-05-03 ) - AWS CloudWatchメトリクスの数式のメモ (
2023-01-17 )