スマートフォンに重要なデータが保存されるのに伴って、セキュリティ意識の高まりが強まっています。その際利用されるのが多要素認証と呼ばれるものです。ID/パスワードの認証に加えて、SMSやワンタイムパスワード、セキュリティキーなどを使って認証を行います。
mBaaSではデフォルトでは追加認証要素には対応していません。そこで、スクリプト機能を使ってワンタイムパスワードを使った認証に対応してみたいと思います。
要件
- mBaaSのID/パスワード認証を使います
- 認証自体を制限するのではなく、特定の重要な機能(決済など)を制限する目的とします
mBaaSでデフォルトで提供しているID/パスワード認証を使いますので、認証自体は多要素認証を使わずに完了してしまいます。そこで、決済やクレジットカード情報の登録など重要な機能を使う場合にワンタイムパスワードを利用する想定で開発します。
QRコードを生成する
ワンタイムパスワードを使う際には、専用のアプリ(Google Authenticator、Authyなど)でQRコードを読み込んで実行するでしょう。まず、このQRコードを生成します。
セキュリティコードの生成
QRコードではユーザごとにランダムなセキュリティコードを用います。そのコードを生成するのに使えるのが speakeasy というライブラリです。
const speakeasy = require('speakeasy');
そして、キーの名前(サービス名)や発行者、文字列の長さを指定してセキュリティキーを生成します。
const secret = speakeasy.generateSecret({ name, issuer, length });
セキュリティキーを生成したら、そのURLを取得してQRコードとして表示します。
const url = speakeasy.otpauthURL({ secret: secret, label: encodeURIComponent(name), issuer: issuer }); new QRCode(document.getElementById("qrcode"), url);
これでQRコードが生成されます。
セキュリティコードを mBaaSに保存する
上記のコードで生成したセキュリティコードは、検証にも用います。そこで、コードをmBaaSに保存しておきます。ユーザからは書き込み(更新)できるように、検証時のために Admin グループに入っているユーザに読み込み権限を付けておきます。
const user = ncmb.User.getCurrentUser(); if (!user) throw new Error("You're not logged in"); const Secret = ncmb.DataStore('Secret'); try { const secret = await Secret .equalTo('userId', user.objectId) .fetch(); if (secret.objectId) { return secret.secret; } } catch (e) { throw e; } const secret = new Secret; const string = speakeasy.generateSecret({ name, issuer, length }); const acl = new ncmb.Acl; acl .setUserReadAccess(user, true) .setUserWriteAccess(user, true) .setRoleReadAccess('Admin', true); await secret .set('secret', string.ascii) .set('userId', user.objectId) .set('acl', acl) .save();
検証する
ここまでのコードでコード管理用アプリに登録まで完了します。次に、コードを入力して検証をします。
スクリプト
サーバでも speakeasy を使います。
const speakeasy = require('speakeasy');
まず管理ユーザでログインします。
const userName = 'admin'; const password = 'admin'; await ncmb.User.login(userName, password);
次にデータストアに保存されているセキュリティキーを取得します。実行時にはクライアントからユーザIDを送信してもらい、それを使ってデータを検索します。
const Secret = ncmb.DataStore('Secret'); const secret = await Secret .equalTo('userId', req.body.user_id) .fetch(); if (!secret.objectId) { return res.status(404).json({message: `User not found. ${req.body.user_id}`}); }
そして検証します。 code
というのがユーザが入力したワンタイムパスワードです。
const code = req.body.code; const verified = speakeasy.totp.verify({ secret: secret.secret, encoding: 'ascii', token: code });
検証結果に応じてレスポンスを返します。
if (verified) { return res.status(200).json({message: 'Verified.'}); } else { return res.status(401).json({message: 'Failed.'}); }
クライアントの実装
スクリプトが totp.js
とした場合、クライアントでは以下のようなコードで検証を実行します。
document.getElementById("verify").onclick = async (e) => { const user = ncmb.User.getCurrentUser(); if (!user) throw new Error("You're not logged in"); const code = document.getElementById("code").value; try { const response = await ncmb.Script .data({ user_id: user.objectId, code: code }) .exec('POST', 'totp.js'); document.getElementById("result").innerHTML = '検証OK'; } catch (e) { document.getElementById("result").innerHTML = '検証NG'; } }
まとめ
今回のコードはNCMBMania/TOTP-Demo: mBaaSでワンタイムパスワードを実現するデモです。にあります。実装時の参考にしてください。アプリのセキュリティは強く求められるようになっています。サーバのセキュリティはmBaaSが提供できますが、アプリのUI/UXでも工夫が必要でしょう。ぜひ皆さんのアプリ開発で役立ててください。