https://s3.amazonaws.com/a.storyblok.com/f/270183/30318/c15ba5c0f9/blog_node-js_verify_1200x600.png

Node.jsとKoa.jsで二要素認証を追加する方法

最終更新日 November 5, 2020

所要時間:7 分

二要素認証(2FA)の名前の由来は、あなたの身元を確認するために2つのものが必要であるという事実からきています。パスワードのような「知っているもの」と、モバイル機器や物理的なトークンの認証コードのような「持っているもの」だ。

アプリケーションに2FAを追加することは難しい作業ではありません。このチュートリアルでは、Vonage Verify API の助けを借りて、あなたのウェブアプリケーションやサービスに 2FA を実装し、セキュリティのレイヤーを追加する方法を説明します。基礎となるメカニズムがどのように機能するかを理解するために、簡単な Koa.js アプリケーションを構築します。これにより、Koa.js を使用していない場合でも、これがあなた自身の既存のプロジェクトにどのように適合するかを簡単に理解することができます。

このチュートリアルでは、Vonage Verify APIとKoa.jsを使った認証トークン・システムの実装方法を説明します。Express.jsを使用した同様のNode.jsチュートリアルがあります。 こちら.

まず、ログイン・ページでユーザーに携帯電話番号の入力を求める。送信すると、SMSで携帯電話番号に送信される認証コードを入力するよう求められる。それが完了すると、アプリケーションにアクセスできるようになります。

前提条件

  • Javascriptの基本的な理解

  • Node.jsマシンにインストールされている

Vonage APIアカウントを取得すると、APIキーとAPIシークレットが Vonage API ダッシュボード.

このチュートリアルでは、ゼロからのプロセスを説明します。完成したコードを見たい場合は git リポジトリをクローンしてください。より大げさなデザインのGlitchバージョンもあります。 リミックスもできます。Glitchの実装では、プロジェクトがプラットフォーム上でどのようにホストされるかに対応するために、若干の違いがあることに注意してください。

Glitch version of demoGlitch version of demo

Koa.jsプロジェクトをゼロから始める

ローカル・マシンにプロジェクト・フォルダーを作成し、以下のコマンドを実行して新しいNode.jsプロジェクトをセットアップする。

npm init

これで一連のプロンプトが表示され、あなたの package.jsonファイルを生成します。必要であれば、答えを空白にしてデフォルト値を使用することもできます。

Configuring package.jsonConfiguring package.json

次に Koa.js.Koaは、ES2015と非同期関数をサポートするために、node v7.6.0以上を必要とすることに注意してください。

npm install koa --save

プロジェクトフォルダーに server.jsファイルを作成する。

touch server.js

新しく作成したファイルに以下のコードを貼り付ける。

const Koa = require('koa')
const port = process.env.PORT || 3000
const app = new Koa()

app.use(async ctx => {
  ctx.body = 'Hello Unicorn 🦄'
})

const listener = app.listen(port, function() {
  console.log('Your app is listening on port ' + listener.address().port)
})

ファイルを実行する。 server.jsファイルを実行する。

node server.js

ブラウザから http://localhost:3000に移動すると、"Hello Unicorn 🦄"というテキストが表示された空のページが表示されるはずです。

Check that server is runningCheck that server is running

をインストールする必要があります。 dotenvをインストールする必要があります。 .envファイルに格納されている環境変数を process.env.

npm install dotenv --save

これで .envファイルには少なくとも以下の変数が含まれていなければならない:

NEXMO_API_KEY='' NEXMO_API_SECRET=''

環境変数にアクセスするには、requireする必要がある。 server.jsファイルの一番上にあるのが理想的だ。

require('dotenv').config()

まだ まだNexmoアカウントに登録していないなら今がそのチャンスです。ダッシュボードにログインすると、API認証情報が最初に表示されます。キーとシークレットは必ず引用符で囲んでください。

プロジェクトの構成

今現在、あなたのプロジェクトにはおそらく package.json, a server.jsファイルと .envファイルだけでしょう。プロジェクト構造をセットアップして、ユーザーがインタラクトできる基本的なフロントエンドができるようにしましょう。

PROJECT_NAME/               
    |-- public/             
    |   |-- client.js
    |   `-- style.css
    |-- views/
    |   `-- index.html
    |-- .env
    |-- package.json
    `-- server.js

そのためには server.jsファイルに手を加えなければならない。 index.htmlファイルと関連するアセットを提供するために、ファイルに手を加える必要があります。Koa.jsはかなり骨太なフレームワークなので、ルーティングや静的アセットを提供するための追加機能は、別途インストールする必要があります。以下は、追加モジュールとその用途のリストです:

  • koa-static静的アセット提供用

  • koa-bodyparserPOSTリクエストで送られたデータを処理する

  • koa-routerルーティング用

  • koa-viewsテンプレートをレンダリングする

この例では Nunjucksを利用しています。Vonage Verify API は SMS 経由で認証コードをトリガーするために使用されるので、Vonage Node.js クライアント・ライブラリもインストールする必要があります。

npm install koa-static koa-bodyparser koa-router koa-views nunjucks nexmo --save

静的アセットとHTMLファイルの提供

スタイルシートやクライアントサイドJavaScriptのような静的アセットを、アプリケーションが /のフォルダから、スタイルシートやクライアント側JavaScriptのような静的アセットを提供できるようにするには、次のように server.jsファイルに追加します:

const serve = require('koa-static')
app.use(serve('./public'))

HTMLファイルを /ビューフォルダからHTMLファイルを提供するには koa-viewsを利用することができます。 render()関数を提供します。この例で使用されているテンプレートエンジンはNunjucksですが、どのテンプレートエンジンが最適かは自由です。

const views = require('koa-views')
app.use(views('./views', { map: { html: 'nunjucks' }}))

次に設定するのは、アプリケーションのページを提供するための基本的なルートです。

const Router = require('koa-router')
const router = new Router()

router.get('/', (ctx, next) => {
  return ctx.render('./index')
})

app.use(router.routes()).use(router.allowedMethods())

この例では、3つのページが必要です。 index.htmlをメインのランディングページとします、 verify.htmlユーザーが認証コードを入力するページ result.html認証が成功したかどうかを表示するページです。

ウェブフォームの構造はいたってシンプルで、CSSで自由に装飾することができます。

<form method="post" action="verify">
  <input name="phone" type="tel" placeholder="+6588888888">
  <button>Get OTP</button>
</form>

このフォームはユーザーの入力を /verifyルートに投稿され、入力された電話番号を使って認証コードのリクエストをトリガーできます。同じようなフォームを他の2つのルート /check/cancelにも同様のフォームが使えます。

<form method="post" action="check">
  <input name="pin" placeholder="Enter PIN">
  <input name="reqId" type="hidden" value="{{ reqId }}">
  <button>Verify</button>
</form>
<form method="post" action="cancel">
  <input name="reqId" type="hidden" value="{{ reqId }}">
  <button class="inline">Cancel verification</button>
</form>

ユーザー入力の処理

それから、ウェブフォーム経由のユーザー入力を処理するために、リクエストを処理するルートが必要になります。 POSTが必要になります。ルーティングの前に bodyparser()を必ず宣言してください。

const bodyParser = require('koa-bodyparser')

/* This should appear before any routes */
app.use(bodyParser())

router.post('/verify/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to trigger verification code here */
})

router.post('/check/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to check verification code here */
})

router.post('/cancel/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to cancel verification code here */
})

ユーザーの電話番号を受信できるようになったので、Verify APIを使ってPINコードを送信する必要があります。Vonage API認証情報を使って新しいNexmoインスタンスを初期化します。

const Nexmo = require('nexmo');
const nexmo = new Nexmo({
  apiKey: YOUR_API_KEY,
  apiSecret: YOUR_API_SECRET
});

注意しなければならない機能が3つある。最初のものは、検証コードをトリガーする nexmo.verify.request()関数で認証コードをトリガーすることです。これにはユーザーの電話番号と、送信者としてユーザーに表示されるブランド名の文字列が含まれる。

async function verify(number) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.request({
      number: number,
      brand: process.env.NEXMO_BRAND_NAME
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

ユーザーがSMSでPINコードを受け取ったら、PINコードを送信してください。 nexmo.verify.check()関数に送信する必要があります。その際 request_idパラメータがあります。この値は、PINコードが正常にトリガーされたときに取得されます。関数にリクエストIDを渡す方法はいくつかあります。 nexmo.verify.check()関数に渡す方法はいくつかありますが、この例では チェックフォームの隠しフィールドを使用します。

async function check(reqId, code) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.check({
      request_id: reqId,
      code: code
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

最後の関数は、ユーザーの気が変わった場合に検証をキャンセルするオプションを提供します。これは nexmo.verify.control()関数を使用し、ここでもPINコードをトリガーして生成されたリクエストIDと、文字列値 cancel.

async function cancel(reqId) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.control({
      request_id: reqId,
      cmd: 'cancel'
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

Landing page for demoLanding page for demo

ここで、先に指定したルートでこれら3つの関数を使用する必要があります。

router.post('/verify/', async (ctx, next) => {
  const payload = await ctx.request.body
  const phone = payload.phone

  const result = await verify(phone)
  const reqId = result.request_id 
  ctx.status = 200
  return ctx.render('./verify', { reqId: reqId })
})

このようになります。 ctx.request.bodyは次のようになる:

{ 
  "phone": "+40987654321"
}

電話番号を取得し、それを verify()関数に渡すことができる。それが有効な電話番号である限り、認証コードが発行され、その応答として request_idそして status.

{ 
  "request_id": "1bf002ecd1e94d8aa81ba7463b19f583",
  "status": "0"
}

そこからリクエストIDをフロントエンドに送信し、ユーザーが検証コードを入力したときに使用することができます。

The request_id is passed to the frontendThe request_id is passed to the frontend

ユーザーが正しいPINを送信したら、そのPINとリクエストIDの両方を check()関数に差し込む必要があります。

router.post('/check/', async (ctx, next) => {
  const payload = await ctx.request.body
  const code = payload.pin
  const reqId = payload.reqId
  
  const result = await check(reqId, code)
  const status = result.status
  ctx.status = 200
  return ctx.render('./result', { status: status })
})

繰り返しになるが、これらの値はいずれも ctx.request.bodyPINが正しいことが検証されると、次のような応答が返ってくる:

{ 
  "request_id": "1bf002ecd1e94d8aa81ba7463b19f583",
  "status": "0",
  "event_id": "150000001AC57AB2",
  "price": "0.10000000",
  "currency": "EUR" 
}

そして、ステータスコードを利用して、ユーザーに表示したいメッセージを決定することができる。この例ではNunjucksを使っているので、結果ページのマークアップは次のようになる:

{% if status == 0 %}
<p>Code verified successfully. ¯\_(ツ)_/¯</p>
{% else %}
<p>Something went wrong… ಠ_ಠ</p>
<p>Please contact the administrator for more information.</p>
{% endif %}

Verification messagesVerification messages

これはコードの各部分を徹底的に分解したものだが、アプリケーション全体がどのように見えるかは、Gitubの のソースコードをご覧ください。.

その他の注意事項

このチュートリアルは、二要素認証の実装に必要な部分だけに焦点を当てた、削ぎ落とされたバージョンです。しかし、実際のアプリケーションでは注意しなければならないことが数多くあります。最も重要なことの一つはエラー処理である。Verify APIは、クエリーが成功した場合はステータス値として 0を返しますが、それ以外の値はエラーを示します。

これらのエラーは処理されるべきであり、フロントエンドのユーザーインターフェイスは、検証の成功を妨げる潜在的なエラーを反映すべきである。また、フロントエンドで何らかの検証を実装したり、Vonageの Number Insight APIを利用して、有効な電話番号のみが Verify API に渡されるようにするのもよいだろう。

次はどこだ?

これらのAPIをもっと使いたいとお考えなら、以下のリンクが参考になるでしょう:

シェア:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing Chenヴォネージの卒業生

ホイ・ジンはNexmoのデベロッパー・アドボケイト。CSSとタイポグラフィをこよなく愛する。