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 {
// その他のエラー
}
}
})();
以上
他にも、ここに書いてないメソッド使ったら都度追記します。