こうこく
作 ▸
改 ▸

AWS Cognito UserPoolをサーバーサイドで使うサンプル (Node.js)

Cognitoが全然分からなくて、クライアント側のJavaScriptで使う記事ばかり読んでしまっていた。

aws-amplify とか amazon-cognito-identity-js でめちゃくちゃ悩んだのに、サーバー側なら普通に aws-sdk を使えばよかったのだった。

Node.js v10.16.3aws-sdk 2.554.0
もくじ

前提: 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 {
			// その他のエラー
		}
	}
})();

レスポンスの例は以下です。ユーザー登録時の戻りと微妙に違うので注意です (AttributesUserAttributes になってる)。

{
    "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 {
			// その他のエラー
		}
	}
})();

以上

他にも、ここに書いてないメソッド使ったら都度追記します。

この記事に何かあればこちらまで (非公開)