JavaScriptからAmazon Cognitoを使うためのまとめ
最低限な要件だけ使ってみるにしても罠だらけだったのでメモ。
調べてもろくな情報出てこなかったので、業務レベルではまじで誰も使ってないんじゃねーのって気持ちがある。
願わくばもう使いたくない( ˘ω˘)
Amazon Cognito is 何
そもそもですが・・。
Amazon Cognitoでできることは、大きく分けて2つです。
- ログイン・セッション機能
- データの同期機能
AWSのコンソールのCognitoのページUI的にも、
- User Pools
- Federated Identities
ってな切り分けになっててそれぞれ対応してる。
まあこのサーバーレスだなんだの時代に、そういうことできるサービスがあることは不思議ではない。
Alternativesという意味では、Firebase Authenticationとかあたり?
JavaScriptのSDKたち
そしてjsで使う上で必要であろうSDKですが、いくつかあります。(=バラバラです)
- aws-sdk: https://github.com/aws/aws-sdk-js
- amazon-cognito-identity-js: https://github.com/aws/amazon-cognito-identity-js
- amazon-cognito-js: https://github.com/aws/amazon-cognito-js
npmからインストールできるそれ関連のパッケージはこの3つ。
使いたい機能によって、必要なライブラリが変わります。
Cognitoを使うには、どちらの機能にしてもコンソールでいくつか作業が必要なのですが、この記事ではコードだけ載せていきます。
なんしかコンソールを通して拾っておくべき情報は以下4つ。
- region
- IdentityPoolId
- UserPoolId
- ClientId
コンソール作業での落とし穴
User Poolsを作ったあと、それを使うためのAppを登録すると思います。
そのときに、クライアントシークレットを「生成しない」ようにする必要があります。
なんかJavaScriptのSDKからは、シークレットを使った認証をサポートしてないらしく。
`1 validation error detected: Value at 'clientId' failed to satisfy constraint`とかいう見当違いのエラーが返ってきたらそれな可能性が高いです。
ちなみに、別App = 別ClientIdになるので、Federated IdentitiesのIdentity Poolの設定としても、別の外部Providerとして追加が必要。
にしてもCognitoのコンソールのUIが使いにくすぎる・・・
User Pools
いわゆるログイン・セッションの機能。
端末またぎで同一ユーザーとしてログイン状態を管理したり、他AWSのAPIを叩くためのトークン取得などに使う。
npm i amazon-cognito-identity-js
検索するといろんなサンプルが出てくるけど、こっちはこれだけでOK。
ただし`webpack` x `json-loader`が必須っぽくて、`browserify`だけでシュッとやるのは無理そう・・?
これはSMS認証する最低限のコード。
まずはプールに登録する。
const { CognitoUserPool, CognitoUserAttribute, } = require('amazon-cognito-identity-js'); const config = require('./config'); const userPool = new CognitoUserPool({ UserPoolId: config.UserPoolId, ClientId: config.ClientId, }); const attributeList = [new CognitoUserAttribute({ Name: 'phone_number', Value: '+8190xxxxyyyy', })]; userPool.signUp(username, password, attributeList, null, (err, result) => { if (err) { console.error(err); return; } const cognitoUser = result.user; });
これでSMSにメールが飛ぶ!ので、そこのコードを見て、
const { CognitoUser, CognitoUserPool, } = require('amazon-cognito-identity-js'); const config = require('./config'); const userPool = new CognitoUserPool({ UserPoolId: config.UserPoolId, ClientId: config.ClientId, }); const cognitoUser = new CognitoUser({ Username: username, Pool: userPool, }); cognitoUser.confirmRegistration(pincode, true, (err, result) => { if (err) { console.error(err); return; } console.info(result); });
これで確認完了。
そうすると、ついにトークンを取りにいけるので・・
const { AuthenticationDetails, CognitoUser, CognitoUserPool, } = require('amazon-cognito-identity-js'); const config = require('./config'); const userPool = new CognitoUserPool({ UserPoolId: config.UserPoolId, ClientId: config.ClientId, }); const cognitoUser = new CognitoUser({ Username: username, Pool: userPool, }); const authenticationDetails = new AuthenticationDetails({ Username: username, Password: password, }); cognitoUser.authenticateUser(authenticationDetails, { onSuccess(result) { const token = result.getIdToken().getJwtToken(); }, onFailure(err) { console.error(err); }, });
晴れてトークンが手に入ったので、これでこの後の処理をなんやかんや。
他のAWSのAPIを叩くときにはこのトークンを渡す。
今回はSMSで認証してるけど、`attributeList`を空にすればID/PWだけでもOKで、最低限のログイン機能は作れる。
他にこのSDKでできることはリポジトリにあるコードサンプルが全てなので、PINコードの再送とか、パスワード忘れたとか、認証要素を追加したいとかは以下リンクを参照。
2017年ですがPromiseなAPIはありません。
Federated Identities
上述の機能とは切り離して使えるデータ同期の仕組み。
存在意義としては、前述のログイン機能と連携することもできるし、連携せずに未認証状態でデータを貯められる。
こっちはこの2つが必要。というのも、
const AWS = require('aws-sdk'); console.log(AWS.CognitoSyncManager); // undefined require('amazon-cognito-js'); console.log(AWS.CognitoSyncManager); // function....
こういうわけ。
必要というより、`AWS.CognitoSync`だけでできる処理をラップしてある感じ。
( ˘ω˘)..。o( いやそれもう本家にいれろよ
使うのは簡単で、
const AWS = require('aws-sdk'); require('amazon-cognito-js'); const config = require('./conf'); AWS.config.region = config.region; AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: config.IdentityPoolId, }); AWS.config.credentials.getPromise() .then(() => { const client = new AWS.CognitoSyncManager(); // コンソールではコレで探すことになる // client.getIdentityId() // 同じものが取れる // AWS.config.credentials.identityId client.openOrCreateDataset('test', (err, dataset) => { if (err) { console.error(err); return; } dataset.get('foo', (err, value) => { if (err) { console.error(err); return; } console.info('get', value); }); dataset.put('foo', 'bar', (err, value) => { if (err) { console.error(err); return; } console.info('put', value); }); // これするまではLocalStorageで完結 dataset.synchronize({}); }); }) .catch(err => console.error(err));
Unauthenticatedでの利用を許可してれば、これだけでデータを貯め続けることができます。
GitHub - aws/amazon-cognito-js: Amazon Cognito Sync Manager for JavaScript
ただこのままだとLocalStorageが消えたらデータにアクセスできなくなるので、なんらかひもづけしたい・・となるはず。
そこで、今まで紹介した2つの合わせ技を使うことで、ログイン状態を保ちつつデータを貯め続けることができます。
ですがその前に・・、
aws/amazon-cognito-js の落とし穴
[1] READMEには載ってないメソッドがいろいろある。
- `putAll()`とか
- `getAll()`とか
必要あればそっちも。
[2] `put()`する時の注意。
dataset.put('文字列以外を', 1, () => {}); dataset.put('putしてしまうと, {}, () => {}); // ここでエラーになってリモートに保存されない dataset.synchronize({});
`synchronize()`しない場合は問題ないです。LocalStorageにデータはちゃんと貯まるし、`put() / get()`の際に`JSON.stringify() / parse()`してくれます。
なのに、`synchronize()`すると突然エラーになります。しかも、`exceeded maximum retry count`とか見当違いのエラーを吐きます。
あと、`put()`の第三引数の空関数は省略できません。(^ω^#)
ログインしてデータ同期
さて、どうやって同一IDでCognito Syncし続けるかでした。
まずさっきの未認証状態でのデータ同期のコードのおさらい。
// まずCredentialsを生成 AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: config.IdentityPoolId, }); // そしてそれを取得して AWS.config.credentials.getPromise() .then(() => { // 取得できたらSync const client = new AWS.CognitoSyncManager(); });
これは未認証状態でも同期できるようにしてるので、Credentialsにはユーザーごとに一意な情報を渡してないです。
なのでココに、ユーザーを判別できるもの = ログイン機能で得られるトークンを渡せばOKということ。
// authenticateしてたら取れる const cognitoUser = userPool.getCurrentUser(); if (cognitoUser === null) { console.log('no user'); return; } cognitoUser.getSession((err, result) => { if (err) { console.error(err); return; } console.log('user', cognitoUser); // ログイン状態でCredentialsを生成 AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: config.IdentityPoolId, Logins: { [`cognito-idp.${config.region}.amazonaws.com/${config.UserPoolId}`]: result.getIdToken().getJwtToken() } }); });
ここまでやれば、あとは同じコードでいけます。
もちろんコンソールで、Identity Poolの外部プロバイダーの設定をしてないとダメです。
なので冒頭の3つのライブラリを全て組合せないといけない・・。
未認証 -> 認証ユーザーへのアップグレード
私はこれで困ったのですが、
- 未認証状態でSyncしてるデータがある
- 認証状態になったときに、そのデータを引き継ぎたい
結論からいうと、それ用のAPIは用意されてないっぽいので、自力でやるしかないです。
FirebaseにはそういうAPIあったのに。
なので流れとしては、
- 未認証状態でSyncしてるデータを`get`
- オンメモリで一時的に保存
- 認証して新しいCredentialsを生成
- 認証状態でさっき一時的に保存したデータを`put`
という手間が必要。ダルい。
`synchronize()`時にコンフリクトする
コンフリクトは、`synchronize()`にコールバックとして解消するためのハンドラを渡す。
これはREADMEにあったコード。
dataset.synchronize({ onSuccess: function(dataset, newRecords) { //... }, onFailure: function(err) { //... }, onConflict: function(dataset, conflicts, callback) { var resolved = []; for (var i=0; i<conflicts.length; i++) { // Take remote version. resolved.push(conflicts[i].resolveWithRemoteRecord()); // Or... take local version. // resolved.push(conflicts[i].resolveWithLocalRecord()); // Or... use custom logic. // var newValue = conflicts[i].getRemoteRecord().getValue() + conflicts[i].getLocalRecord().getValue(); // resolved.push(conflicts[i].resolveWithValue(newValue); } dataset.resolve(resolved, function() { return callback(true); // onSuccessへ }); // Or... callback false to stop the synchronization process. // return callback(false); // onFailureへ }, onDatasetDeleted: function(dataset, datasetName, callback) { // Return true to delete the local copy of the dataset. // Return false to handle deleted datasets outsid ethe synchronization callback. return callback(true); }, onDatasetsMerged: function(dataset, datasetNames, callback) { // Return true to continue the synchronization process. // Return false to handle dataset merges outside the synchroniziation callback. return callback(false); } });
わかる・・わかるけど解せぬ・・。