https://s3.amazonaws.com/a.storyblok.com/f/270183/27130/7c8cdb3eba/onetimepassword.png

Dispatch APIを使ってワンタイムパスワード(OTP)サービスを構築する

最終更新日 October 23, 2020

所要時間:3 分

製品廃止のお知らせ

2025年8月31日以降、Vonage Dispatch APIは新規ユーザーに対してクローズされますが、既存ユーザーに対しては引き続きサポートされます。フェイルオーバー機能を持つメッセージング・アプリケーションを構築したい場合、フェイルオーバーは現在Messages APIで直接サポートされています。

メッセージ・フェイルオーバー機能に関する一般的な情報については、本ガイドを参照してください。 このガイド.Dispatch APIからMessages API Failoverへの移行については、以下を参照してください。 このガイド.

この非推奨製品に関するご質問は、下記までお問い合わせください。 にご連絡ください。 Vonage Community Slackまでお問い合わせください。

ワンタイムパスワード(OTP)は、主に従来のパスワードでは保証されないセキュリテ ィ要件により、最近非常によく知られるようになった。従来のパスワードの保護はユーザーの責任であり、ユーザーは十分な注意を払わないことが多いのは周知の通りであるが、OTPはランダムに生成され、その有効期限も限られているため、実質的に自己保護されている。

従来のパスワードの代わりにOTPを使用したり、二要素認証(2FA)アプローチで従来の認証プロセスを強化することができます。実際、メールボックス、電話、特定のアプリなど、ユーザー自身が所有する通信媒体に依存することで、ユーザーのアイデンティティを保証するメカニズムが必要な場合は、どこでもOTPを使用できます。

この記事では、2つのWeb APIに基づいて基本的なワンタイムパスワードサービスを実装する方法を紹介する:

  • 最初のAPIでは、OTPを作成し、主要な媒体としてFacebook Messenger経由で、または予備の媒体としてSMS経由でユーザーに送信することができます。

  • 2番目のAPIは、ユーザーが受け取ったOTPをVerifyすることを可能にする。

OTPサービスにはユーザー・インターフェースはありません。OTPサービスは、アプリケーションから呼び出してOTPを生成・検証できるマイクロサービスとして設計されています。

前提条件

この記事で紹介するワンタイムパスワードサービスを利用するには、以下のものが必要です:

プロジェクトのセットアップ

最初のステップとして、GitHubリポジトリからプロジェクトをクローンするかダウンロードする必要がある。

プロジェクトのコードをコンピューターに入手したら、プロジェクトのフォルダーに移動して以下のコマンドを入力し、依存関係をインストールする必要がある:

npm install

後で見るように、このアプリケーションは、ユーザーにOTPを送信するために、WebフレームワークとしてExpressを使用し、Node.js用のVonageクライアント・ライブラリを使用している。

アプリケーションの設定

OTP サービスを使用する前に、Vonage API ダッシュボードでいくつかの設定を行い、以下の方法でメッセージ配信を有効にする必要があります。 Vonage Dispatch API.

このAPIを使うと、複数のチャネルを使って優先順位をつけてユーザーにメッセージを送ることができます。例えば、私たちのケースでは、最初の試みとして、Messengerアカウント経由でユーザーにOTPを送信します。ユーザーが一定時間内にそれを読まなかった場合、メッセージはユーザーの電話番号にSMSで送信されます。

そこで、Vonage APIダッシュボードにアクセスし、メニューの「Messages and Dispatch」項目を選択し、「Create Application」を選択します。 アプリケーションの作成を選択します:

Vonage DashboardVonage Dashboard

Vonage API用語では、アプリケーションはMessages APIとDispatch APIを使用するためのデータの束です。上のフォームからわかるように、最小限のデータは以下の通りだ:

  • アプリケーション名

  • メッセージの配信ステータスを受け取るために有効化されたパブリックウェブフックのURL

  • 受信メッセージの受信が可能なパブリックWebhookのURL

  • APIに送信するリクエストの署名に使用する公開鍵(テキストエリアの下にあるリンクをクリックすると、公開鍵と秘密鍵のペアを生成できます。)

メッセージアプリケーション作成プロセスのステップ2と3では、Messenger、WhatsApp、Viberなどの外部アカウントに電話番号やリンクを割り当て、Message and Dispatch APIを通じてSMSやメッセージを送信できるようにします。

特に このドキュメントをご参照ください。

Vonage APIアプリケーションに提供されるステータスと受信URLは、一般にアクセス可能でなければならないことに注意してください。パブリックなウェブサーバを持っていない場合や、自分のコンピュータで試したい場合は ngrokを使ってください。

Ngrok での作業の詳細については、次のドキュメントを参照してください。 ドキュメンテーション.ngrokの無料プランを使用する場合、ツールを実行するたびに一時的なURLが生成されることに留意してください。そのため、ダッシュボードの設定でアプリケーションのURLを適宜更新する必要があります。

Vonage APIアプリケーションを作成すると、そのアプリケーションに Application IDが割り当てられます。それを覚えておいてください。

OTPサービスの設定

Vonage側を設定したら、OTPサービス側を設定して相互に通信できるようにする必要があります。

そこで nexmo.jsonファイルを開き srcファイルを開き、要求されたデータを提供する:

{
   "apiKey": "YOUR_API_KEY",
   "apiSecret": "YOUR_API_SECRET",
   "applicationId": "YOUR_APPLICATION_ID"
}

を取得することができます。 apiKeyapiSecretの値は Vonage API ダッシュボードの設定セクションから取得できます。 applicationIdは前のセクションでメモした値です。

次に、アプリケーションに割り当てた公開鍵に関連付けられた秘密鍵を取り出し、srcフォルダの下の private.keyファイルに格納します。

OTPサービスの実行

いよいよOTPサービスを実行します。プロジェクトのルート・フォルダーに以下のコマンドを入力してください:

npm start

しばらくすると、サーバーがポート3000で動作しているというメッセージが表示されるはずです。ブラウザーをそのアドレスに向けることで、機能しているかどうかを確認できる。 http://localhost:3000アドレスにブラウザを向けることで動作しているかどうかを確認できます。問題がなければ、「This is the OTP service」というメッセージが表示されるはずです。

OTPのリクエスト

さて、アプリケーションが、ユーザーの身元を確認するために、ユーザーに送信する OTP を生成する必要があるとします。そのためには POSTリクエストの識別子となる文字列と、OTPを送信するユーザーの連絡先を指定して、OTPサービスにリクエストを送信する必要があります。

実行中の OTP サービスに以下のような HTTP リクエストを送信することで実行できる:

POST /otp/123456789 HTTP/1.1
Host: localhost:3000
Content-Type: application/json
cache-control: no-cache
{"messengerId": "8192836451", "phoneNumber": "393331234567"}

API URIに付けられた文字列 123456789はリクエストの識別子です。私たちはこれを tokenと名付けます。リクエストの本文には、メッセンジャーの識別子とOTPを受け取るユーザーの電話番号を含むJSONオブジェクトが含まれます。

次の図に示すように、Postman経由でリクエストを送信することができます:

PostmanPostman

OTPサービスは5桁の数字からなるOTPを生成し、指定されたメッセンジャーの識別子に送信します。後ほど説明しますが、ユーザーが一定時間内にOTPを読まなかった場合、OTPはSMSで電話番号に送信されます。

OTP の作成に成功すると、201 Created HTTP ステータスコードが返されます。

OTPのベリファイ

メッセージを受信した媒体に関係なく、ユーザーは、以下の例のように、2つ目の API に GET リクエストを送信して OTP を検証する必要がある:

GET /otp/123456789/63731 HTTP/1.1
Host: localhost:3000
cache-control: no-cache

APIのURIは otp接頭辞、リクエスト・トークン(OTP の作成をリクエストしたときに提供されたリクエスト識別子)、そして OTP そのものによって構成されます。

ポストマン』では次のように書かれている:

PostmanPostman

このようなリクエストを送信すると、レスポンスとして以下のHTTPステータスコードのいずれかが返されます:

  • 200 OK- OTP が有効な場合、次のようなレスポンスが返されます。

  • 404 見つかりません- ワンタイムパスワードが間違っている場合、つまりワンタイムパスワードサービスによってワンタイムパスワードが生成されていない場合、このようなレスポンスが返されます。

  • 409 コードはすでに検証済みです- この応答は、あなたまたは他の誰かがすでにOTPを検証したことを意味します。

  • 410 コードの有効期限が切れています- 有効期限を過ぎたOTPをVerifyしようとすると、次のような応答が返ってきます。

また 404 コードが無効です。その他の理由でOTPサービスがコードを検証できない場合、HTTPステータスコードが表示されます。

仕組み

OTPサービスを実装するコードを見てみましょう。次の図は、プロジェクトに属するフォルダとファイルをまとめたものです:

Project structureProject structure

その index.jsファイル srcファイルには、アプリケーションの開始コードとWeb APIの定義が含まれています。作成APIと検証APIは以下のコードで実装されています:

app.post("/otp/:token", (req, res) => {
  const otp = otpManager.create(req.params.token);
  otpSender.send(otp, req.body);
  res.sendStatus(201);
 });

 app.get("/otp/:token/:code", (req, res) => {
    const verificationResults = otpManager.VerificationResults;
    const verificationResult = otpManager.verify(req.params.token, req.params.code);
    let statusCode;
    let bodyMessage;

    switch (verificationResult) {
      case verificationResults.valid:
        statusCode = 200;
        bodyMessage = "OK";
        break;
      case verificationResults.notValid:
        statusCode = 404;
        bodyMessage = "Not found"
        break;
      case verificationResults.checked:
        statusCode = 409;
        bodyMessage = "The code has already been verified";
        break;
      case verificationResults.expired:
        statusCode = 410;
        bodyMessage = "The code is expired";
        break;
      default:
        statusCode = 404;
        bodyMessage = "The code is invalid for unknown reason";
  }
  res.status(statusCode).send(bodyMessage);
});

おわかりのように、どちらのAPIも、実際にOTPを作成し検証するために otpManagerに依存しています。 otpSenderに依存している。これらの初期化は数行上の同じ index.jsファイルで行われる:

const OtpManager = require("./OtpManager");
const otpRepository = require("./otpRepository");
const otpSender = require("./otpSender")

const otpManager = new OtpManager(otpRepository, {otpLength: 5, validityTime: 5});

ここでは、サービス全体が3つのコンポーネントで構成されていることがわかる:

  • otpManagerワンタイムパスワードの作成と検証を担当

  • otpRepositoryOTPの持続を担当

  • otpSenderユーザーへのOTP送信を担当

これら3つのコンポーネントが存在することで、作成と検証、保管、配送の実装をそれぞれ独立させることができる。

のインスタンスを作成するときに otpManagerのインスタンスを作成するときに otpRepositoryoptionsオブジェクトを渡し、OTPの長さ(5文字)と有効とみなされる時間(5分)を指定する。

otpManager

のインスタンスである。 otpManagerのインスタンスである。 OtpManagerクラスのインスタンスである。 OtpManager.jsファイルで実装されたクラスのインスタンスである。主なメソッドは create()verify().

この create()メソッドは新しい OTP を生成し、以下のように実装される:

create(token) {
  const code = Math.floor(Math.random()*Math.pow(10, this.options.otpLength))
    .toString()
    .padStart(this.options.otpLength, "0");

  let otp = new OtpItem(token, code);
  this.otpRepository.add(otp);

  return otp;
}

トークンを入力とし、5桁の乱数を生成する。文字列に変換し、"0 "文字でパディングすることで、結果のコードが正確に5桁で構成されていることを保証する。

もちろん、これはOTP作成の非常に単純な実装である。もっと正確な より正確なアルゴリズムを実装したいかもしれないが、これはこの記事の範囲外である。

このコードが生成されると、クラスのインスタンスとしてotpオブジェクトが生成されます。 OtpItemクラスのインスタンスとして otpインスタンスを otpRepository.

この OtpItemクラスは、OTPの関連情報を表す構造を定義し、次のファイルに実装されています。 OtpItem.jsファイルに実装されている:

class OtpItem {
  constructor(token, code) {
    this.token = token;
    this.code = code;
    this.creationDate = new Date();
    this.isChecked = false;
    this.checkDate = null;
  }
}

この verify()メソッドは、渡されたトークンのコードが生成されたかどうか、そしてそれがまだ有効かどうかをチェックします。以下はその実装です:

verify(token, code) {
    const id = `${token}-${code}`;
    const otp = this.otpRepository.getById(id);
    let verificationResult = VerificationResults.notValid;
  
    if (otp) {
      switch (true) {
        case otp.isChecked:
          verificationResult = VerificationResults.checked;
          break;
        case isOtpExpired(otp, this.options.validityTime):
          verificationResult = VerificationResults.expired;
          break;
        default:
          otp.isChecked = true;
          otp.checkDate = new Date();
          this.otpRepository.update(otp);
          verificationResult = VerificationResults.valid;
  
      }
    }
  
    return verificationResult;
  }
}

このメソッドは、トークンとコードを連結して OTP 識別子を構築する。この識別子は otpインスタンスを otpRepository.このようなインスタンスが存在する場合、このメソッドは、そのインスタンスが検証済みかどうか、および有効期限が切れていないかどうかを検証します。返される値は、OTP の有効性ステータスを表す列挙値です。

otpRepository

otpRepositoryのインスタンスを OtpItemのインスタンスを、プレーンなJSONファイルとしてファイルシステムの otpItemsフォルダに格納する。これは、デモケースとしては非常にシンプルなソリューションです。データをデータベースに保存することで実装することもできます。

以下はその実装コードである。 otpRepository.jsファイルにある実装コードです:

const fs = require("fs");
const path = require("path");

const baseRepositoryPath = "./otpItems";

function add(otpItem) {
  checkBaseFolder();
  fs.writeFileSync(path.join(baseRepositoryPath, `${otpItem.token}-${otpItem.code}`), JSON.stringify(otpItem));
}

function getById(id) {
  const content = getFileContent(path.join(baseRepositoryPath, id));
  let otpItem = null;
  
  if (content) {
    otpItem = JSON.parse(content);
  }

  return otpItem;
}

function update(otpItem) {
    fs.writeFileSync(path.join(baseRepositoryPath, `${otpItem.token}-${otpItem.code}`), JSON.stringify(otpItem));

    return otpItem;
}

function checkBaseFolder() {
  if (!fs.existsSync(baseRepositoryPath)){
    fs.mkdirSync(baseRepositoryPath);
  }
}

function getFileContent(fileName) {
  let content = null;
  
  try {
    content = fs.readFileSync(fileName);
  } catch (error) {
    console.log(error);
  }

  return content;
}

module.exports = {
  getById,
  add,
  update
};

ご覧のように getById()メソッドを実装しています。 OtpItemインスタンスを取得するメソッド add()インスタンスを格納する OtpItemそして update()メソッドを実装している。

otpSender

コンポーネントが otpSenderコンポーネントは、Vonage Dispatch API を使用してユーザーに OTP を送信します。これは otpSender.jsファイルで実装されています:

const Nexmo = require('nexmo')
const nexmoConfig =require("./nexmo.json");
const path = require("path");

nexmoConfig.privateKey = path.join(__dirname, "private.key");

const nexmo = new Nexmo(nexmoConfig);

function send(otp, recipientAdresses) {
  const message = `Insert the following code: ${otp.code}`;

  nexmo.dispatch.create("failover", [
    {
      "from": { "type": "messenger", "id": "YOUR_MESSENGER_ID" },
      "to": { "type": "messenger", "id": recipientAdresses.messengerId },
      "message": {
        "content": {
          "type": "text",
          "text": message
        }
      },
      "failover":{
        "expiry_time": 120,
        "condition_status": "read"
      }
    },
    {
      "from": {"type": "sms", "number": "NEXMO"},
      "to": { "type": "sms", "number": recipientAdresses.phoneNumber},
      "message": {
        "content": {
          "type": "text",
          "text": message
        }
      }
    },
    (err, data) => {
      console.log(data.dispatch_uuid);
    }
  ])  
}

module.exports = {
  send
};

ファイルからのデータと nexmo.jsonファイルと private.keyファイルからのデータをマージしてコンフィギュレーションを構成します。このコンフィギュレーションは、ライブラリのコンストラクタに渡されて nexmoインスタンスを生成します。このインスタンスは send()関数の実装で使用されます。この関数は OtpItemインスタンスと recipientAddressesオブジェクトを引数にとり、ユーザーに送信するメッセージと Dispatch API 用のペイロードを作成します。

この nexmo.dispatch.create()メソッドを通して、フェイルオーバー機能を持つ配信ワークフローを作成します。メソッドの第二引数は、3つの項目を含む配列である:

  • 最初の項目は、送信者、受信者、送信するメッセージのテキストを指定するオブジェクトです。送信者と受信者のタイプは、これがメッセンジャー通信であることを示している。また failoverプロパティを持っています。この例では、メッセージが120秒以内に読まれなかった場合に失敗とみなされます。

  • つ目の項目は、送信者、配送先、Messengerの配送に失敗したときに送るメッセージのテキストを指定する別のオブジェクトです。この場合、送信者と受信者のタイプは、メッセージがSMSで送信されることを示しています。

  • 最後の項目は、ワークフローがVonage APIサーバーに送信された後に実行されるコールバック関数です。我々の場合、単純にコンソールにワークフロー識別子 (dispatch_uuid) をコンソールに書き込みます。

これにより、使用される通信媒体に関係なく、生成されたOTPがユーザーに配信される可能性が高まる。

シェア:

https://a.storyblok.com/f/270183/384x384/373925f138/andrea-chiarelli.png
Andrea Chiarelli

Andrea Chiarelli has over 20 years of experience as a software engineer and technical writer. Throughout his career, he has used various technologies for the projects he was involved in. Currently, he is a software architect at the Italian office of Apparound and contributes to a few online magazines and blogs.