https://a.storyblok.com/f/270183/75769/d4161950b8/group-authentication.png

SMSとExpressによる信頼できるグループ認証

最終更新日 April 10, 2024

所要時間:4 分

はじめに

一人でプロジェクトを進めるのは、時に退屈で孤独なものだ。だからこそ、友人と協力することで、プロセスをより楽しくすることができる!しかし、ミーティングをアレンジしたり、みんなの都合のいい時間を見つけるのは面倒なことです。ありがたいことに、オンラインでシームレスに共同作業ができるウェブアプリケーションを作るという解決策があります。

このチュートリアルでは、Vonage Verify APIを使用したシンプルな認証フローの作成方法を説明します。これにより、複雑なパスワードポリシーや暗号化を扱うことなく、友人が安全に連携アプリにアクセスできるようになります。その代わりに、電話番号とVonageの認証システムを活用して認証し、セッションCookieを介してログイン状態を維持します。

tl;drIアプリケーションをスキップしてすぐにデプロイしたい場合は、必要なコードはすべて 必要なコードはすべて GitHub にあります。.

前提条件

その前に、あなたが持っていることを確認してください:

  1. Node.jsとnpmをインストール

  2. Express.jsとSQLiteの基本的な知識

  3. A Vonage APIアカウント

  4. SMS機能付きバーチャルVonage番号

オプションとして ngrokのようなツールを使うこともできます。

セッティング

新しいNode.jsプロジェクトを作成し、必要なパッケージをインストールすることから始めます:

npm init npm install express sqlite3 connect-sqlite3 cookie-parser express-session @vonage/server-sdk

これにより、Express、SQLite3データベースのサポート、セッション/クッキー用ミドルウェア、Vonage APIクライアント・ライブラリが追加されます。

次に .envファイルを作成し、Vonageの認証情報とバーチャル番号を追加します:

API_KEY="YOUR_API_KEY" API_SECRET="YOUR_API_SECRET" APP_NUM="YOUR_VIRTUAL_NUMBER"

また、セッション保存用の秘密鍵も生成しておきたい:

SESH_SECRET="a_very_complex_random_string"

招待コード、プロジェクト・ドメイン、VonageアプリIDなど、必要なその他の環境変数を設定します。

ADMINS="kelly,michelle,beyonce" INVITE_CODE="code123" PRIVATE_KEY_PATH=private.key APP_ID=49f7d24b-42343ds-vs234-vxcsfds PORT=3003

エクスプレスの設定

メイン・サーバー・ファイル(例. app.js) で、Express を要求し、基本サーバをセットアップします:

const express = require('express');
const app = express();
const port = 3000;
 
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

次に、JSONリクエストを解析し、セッションを管理し、SQLite3セッションストレージを有効にするミドルウェアを追加します:

// parse client requests in JSON
app.use(express.json());
 
// install packages to do session management
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
 
// configure automatic session storage in SQLite db
app.use(require('cookie-parser')());
app.use(session({
  store: new SQLiteStore,
  secret: process.env.SESH_SECRET || 'your-secret-key-here', // Provide a secret option
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 1 week
}));

API クレデンシャルで Vonage Server SDK オブジェクトを初期化することを忘れないでください:

const Vonage = require('@vonage/server-sdk');
const vonage = new Vonage({
  apiKey: process.env.API_KEY,
  apiSecret: process.env.API_SECRET, 
  applicationId: process.env.APP_ID,
  privateKey: process.env.PRIVATE_KEY_PATH,
});

SQLiteデータベースのセットアップ

認証フロー中のユーザーデータとセッションを保存するために、いくつかのSQLiteテーブルが必要です:

const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(':memory:');
 
db.serialize(function(){
  if (!exists) {
    db.run('CREATE TABLE Sessions (phone NUMERIC, id TEXT)');
    db.run('CREATE TABLE allowlist (phone NUMERIC, username TEXT)');
    db.run('CREATE TABLE Authors (username TEXT)');
  }
});

この Sessionsテーブルは、検証中のユーザーの電話番号とVonageリクエストIDを一時的に保持します。テーブル Allowlistは、許可されたサインアップを追跡します。 Authorsは認証されたユーザ名の永続化されたリストです。

ハンドル・ルート

眺望のためのルート

あなたのサーバーはすでにメインページのルートを /.その下に、ユーザー用のサインアップもしくはログインページと、あなた用の管理ページ用の2つのルートを追加できます:

// View Routes
app.get('/', function(request, response) {
  response.sendFile(__dirname + '/views/index.html');
});
 
app.get('/signup', function(request, response) {
  response.sendFile(__dirname + '/views/signup.html');
});
 
app.get('/admin', function(request, response) {
  if (isAdmin(request.session)) {
    response.sendFile(__dirname + '/views/admin.html');
  } else {
    response.sendFile(__dirname + '/views/index.html'); 
  }
});

管理機能へのアクセス管理を始めるには、次のように宣言する必要があります。 isAdmin関数を宣言しなければなりません。 /adminルートで参照される関数を宣言する必要があります。から管理者のリストを配列に分割し、現在のセッションのユーザー名と完全に一致するものを探します。 .envの管理者リストを配列に分割し、現在のセッションのユーザー名と完全に一致するものを探します:

function isAdmin(sesh) {
  let admins = process.env.ADMINS.split(',');
  return admins.includes(sesh.username);
}

管理者エンドポイント

ユーザを追加するワークフローの最初のステップは、管理者がそのユーザの電話番号を許可リストに追加することです。同じ人が異なるデバイスからログインしたい(そのため追加のセッションクッキーが必要)、あるいはセッションが期限切れになるかもしれないので、管理者はオプションで電話番号を既存のユーザ名に関連付けることができます。

まず、エンドポイントを /inviteでエンドポイントを宣言し、この人がまだ管理者であることを確認するためのセキュリティチェックを追加する:

app.post('/invite', function(request, response) {
  if (!isAdmin(request.session)) {
    response.status(500).send({message: "Sorry, you're not an admin"});
    return;
  }
 
});

招待を追加しようとする人が管理者でない場合、リクエストは失敗するはずです。

この関数が次にすることは、リクエストから電話番号を取得することである。軽い妥当性チェックの後、許可リストに追加されます。管理者がユーザー名を指定していれば、それも追加されます:

app.post('/invite', function(request, response) {
  if (!isAdmin(request.session)) {
    ...
  }
  let phone = request.body.phone;
  if (!isNaN(phone)) {
    if (request.body.username) {
      db.run('INSERT INTO Allowlist (phone, username) VALUES ($phone, $user)', {
        $phone: phone,
        $user: request.body.username
      });
    } else {
      db.run('INSERT INTO Allowlist (phone) VALUES ($phone)', {
        $phone: phone
      });
    }
 
  }
});

新しい電話番号が許可リストに追加されたら、最後に行うことは、新しいユーザーに招待メールを送ることです。に保存した電話番号からテキストが送信されます。 .envで保存した電話番号から送信され、ユーザーは現在の招待コードを受信して返信メールを送り返します。

このステップを完全にスキップして、認証PINを送信することもできます。しかし、これにより、サインアップURLのような、ユーザにとって有益と思われるコンテキスト情報を提供することができます。Vonage Verify PINは5分間しか有効ではないので、受信者がPINを見る前に失効しないようにすることもできます:

app.post('/invite', function(request, response) {
  if (!isAdmin(request.session)) {
    ...
  }
  let phone = request.body.phone;
  if (!isNaN(phone)) {
    if (request.body.username) {
      ...
    } else {
      ...
    }
    vonage.messages.send(
      new SMS(
        `Please reply to this message with "${process.env.INVITE_CODE}" to get your PIN.`,
        phone,
        process.env.APP_NUM,
      ),
    );
  }
});

ウェブフック・エンドポイント

Vonage Messages APIで着信テキストを受信する前に、Vonageダッシュボードでアプリケーション設定を構成する必要があります。仮想番号を購入し、その番号のコンフィギュレーションの下にある Inbound Webhook URL を、Node.js サーバーを公開するローカルの ngrok URL に設定します。

例えば、Inbound Webhook URLを次のように設定します。 http://1234abc.ngrok.io/answerを設定して、受信メッセージをローカルの /answer エンドポイントに送信します。

電話番号を設定したら、Webhookエンドポイントのロジックを追加します。テキストに現在の招待コードが含まれていることと、その招待元電話番号が許可リストに含まれていることを確認します。これらの条件が満たされていれば、確認のリクエストを送信し、応答で得た電話番号とIDをSessionsデータベースに保存します:

app.post('/answer', function(request, response) {
  let from = request.body.from;
  if (request.body.text === process.env.INVITE_CODE) {
    db.all('SELECT * from Allowlist WHERE phone = $from', 
      {$from: from}, 
      function(err, rows) {
      if (rows.length) {
        vonage.verify.request({
          number: from,
          brand: process.env.PROJECT_DOMAIN
        }, (err, result) => {
          db.run('INSERT INTO Sessions (phone, id) VALUES ($phone, $id)', {
            $phone: from,
            $id: result.request_id
          });
          response.status(204).end();
        });
      }
    });
  }
});

今回、新規ユーザーは、Vonage Verifyによって自動的に生成されたPINを含むテキストを受け取ります。あなたのアプリケーションの電話番号と ngrok のドメインを識別するために提供しましたが、それ以外は定型文です。このメッセージには、ユーザーが応答できるものが必要です。その記録が保存された状態で、ユーザーがウェブアプリを通じて最後のステップを完了するのを待つ。

サインアップまたはログインのエンドポイント

新規ユーザーはウェブクライアントから電話番号、ユーザー名、PINを送信します。保存されるのはユーザー名のみです。他の値は認証プロセスのためのもので、このログインが成功したらデータストアから削除します。

新しい /loginを追加し、 最初のステップとしてユーザ名の簡単な検証を行います。この例では、基本的な文字のみを許可しています。ユーザ名が検証されると、提供された電話番号のセッションが見つかります:

app.post('/login', function(request, response) {
  let allowed = RegExp('[A-Za-z0-9_-]+');
  let username = request.body.username;
  if (!allowed.test(username)) {
    return;
  }
  db.each('SELECT * FROM Sessions WHERE phone = $phone', {
    $phone: request.body.phone
  }, function(error, sesh) {
 
  }); 
});

セッション行を提供するコールバックの中で、ユーザ名をもう一回チェックします。今回は、そのユーザ名がすでに使用されているかどうか、もし使用されていれば、この電話番号がそのユーザ名でのログインを許可されているかどうかを確認します:

app.post('/login', function(request, response) {
  ...
  db.each('SELECT * FROM Sessions WHERE phone = $phone',{
    $phone: request.body.phone
  }, function(error, sesh) {
 
    let broken = false;
    db.all('SELECT * FROM Authors WHERE username = $user', {
      $user: username
    }, function(err, rows) {
 
      if (rows.length) {
        db.all('SELECT * FROM Allowlist WHERE username = $user AND phone = $phone', {
          $user: username,
          $phone: sesh.phone
        }, function(e, r) {
          if (e || !r.length) broken = true; 
        });
      }   
    });
 
    if (!broken) {
 
    }
  }); 
});

ユーザー名のチェックがすべてパスした場合、フラグを使用して続行してよいことを確認し、 クライアントから受け取ったPINがこの検証リクエストに対して正しいことを確認する。もし正しければ、応答のステータスは0となり、このプロセスで使用したセッションとallowlistレコードを安全に削除することができる。最後のステップは、セッションにユーザー名を入れることである:

app.post('/login', function (request, response) {
  let allowed = RegExp('[A-Za-z0-9_-]+');
  let username = request.body.username;
  if (!allowed.test(username)) {
    response.status(500).send({ message: 'Please use basic characters for your username' });
    return;
  }
  db.each('SELECT * FROM Sessions WHERE phone = $phone', {
    $phone: request.body.phone
  }, function (error, sesh) {
 
    db.all('SELECT * FROM Authors WHERE username = $user', { $user: username }, function (err, rows) {
      if (rows.length) {
        db.all('SELECT * FROM Allowlist WHERE username = $user AND phone = $phone', {
          $user: username,
          $phone: sesh.phone
        }, function (e, r) {
          if (e || !r.length) {
            response.status(500).send({ message: 'Please choose a different username' });
            return;
          }
        });
      }
 
      vonage.verify.check(sesh.id, request.body.pin)
        .then(result => {
          if (result && result.status === '0') {
            db.serialize(function () {
              db.run('INSERT INTO Authors (username) VALUES ($user)', {
                $user: username
              });
              db.run('DELETE FROM Allowlist WHERE phone = $phone', {
                $phone: sesh.phone
              });
              db.run('DELETE FROM Sessions WHERE phone = $phone', {
                $phone: sesh.phone
              });
            });
            request.session.username = username;
            response.status(200).send({ message: "Success" });
          }
        })
        .catch(err => {
          // handle errors
          console.error(err);
 
          if (err) {
            console.log('Error occurred:', err);
            response.status(500).send({ message: 'Error verifying your info' });
          }
        });
    });
  });
});

マークアップの追加

クライアント側でデータを収集するためには、管理フォームとサインアップフォームの2つの同じようなフォームが必要です。管理フォームは新しいユーザーへの招待をトリガーし、登録フォームは新しいセッションを作成します。index.htmlページはプロジェクトのランディングページとなります。しかし、index.htmlの内容をadmin.htmlとsignup.htmlにコピーしておくと、足場が整って便利です。

admin.htmlの <main>タグの中で、HTMLを電話番号とユーザー名を収集するシンプルなフォームに置き換える:

<main>
  <h2>
    Invite people to your application  
  </h2>
 
  <form action="/invite" method="post">
    <label>Phone number:
      <input type="phone" id="phone" name="phone" />
    </label>
 
    <label>Username (optional):
      <input type="text" id="username" name="username" />
    </label>
 
    <input type="submit" value="Invite" id="invite_btn" />
    <h3 id="feedback"></h3>
  </form>
</main>

signup.htmlの <main>の内容は非常に似ているはずだが、そこではPINも集めることになる:

<main>
  <h2>
    Sign up or log in
  </h2>
 
  <form action="/login" method="post">
    <label>Phone number:
      <input type="phone" id="phone" name="phone" />
    </label>
 
    <label>Username:
      <input type="text" id="username" name="username" />
    </label>
 
    <label>PIN:
      <input type="text" id="pin" name="pin" />
    </label>
        <input type="submit" value="Sign up" id="signup_btn" />
        <h3 id="feedback"></h3>
      </form>
</main>

デフォルトのHTML構造を維持することで、両ページにまたがってリンクすることができます。 client.jsリンクが継続されます。これらのページのフォームは同じようなデザインなので、1つのスクリプトファイル client.jsで効率的に管理できます。リセットして client.jsを空白の状態にリセットした後、カスタマイズしたスクリプトを実装するのに最適な場所です。

このスクリプトでは、まず、対話に不可欠なフォーム要素を特定して収集する。次に、両方のフォームの送信ボタンを検出し、クリックイベントリスナーを確立します。これらのリスナーは、サーバーへのフォーム・データ送信を処理する共通の関数を呼び出すと同時に、標準的なフォーム送信の動作をインターセプトして阻止します。純粋にHTMLだけでフォーム送信を管理することも可能ですが、このタスクにJavaScriptを採用することで、特にアプリケーションが進化し、より洗練された処理が要求されるようになったときに、柔軟性が高まります。

let phone = document.querySelector('#phone');
let username = document.querySelector('#username');
let feedback = document.querySelector('#feedback');

// Invite Form
let invite_btn = document.querySelector('#invite_btn');
if (invite_btn) {
  invite_btn.onclick = function(e) {
    e.preventDefault();
    let body = JSON.stringify({
      phone: phone.value,
      username: username.value
    });
    goFetch('/invite', body, e.target);
    return false;
  };
}

// Sign Up From
let signup_btn = document.querySelector('#signup_btn');
if (signup_btn) {
  signup_btn.onclick = function(e) {
    e.preventDefault();
    let body = JSON.stringify({
      phone: phone.value,
      username: username.value,
      pin: document.querySelector('#pin').value
    });
    goFetch('/login', body, e.target);
    return false;
  };
}

function goFetch(url, body, btn) {
  fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: body
  })
  .then(response => response.json())
  .then(data => {
    feedback.innerText = data.message || 'Thank you!';
    btn.style.display = 'none';
  });
}

プロジェクトの実行

プロジェクトを開始するには、ngrokが動いているのと同じポートでターミナルから'npm run start'を実行する。

注:アプリケーションの準備はできましたが、問題があります:他の人を招待するには管理者権限が必要ですが、この権限を得るには招待が必要です。この問題に対処するには、コードに完全にアクセスできる開発者(あなたのような)のために回避策を作る必要があります。手っ取り早い解決策は、コードの isAdmin関数を return trueに変更し、一時的に無制限のアクセスを許可することです。

結論と次のステップ

このチュートリアルはここまでです!エラー処理を追加することで、すでに使用されているユーザー名を選択したり、無効な文字を入力したりするようなユーザーインタラクションを洗練させることができます。さらに、ユーザー管理エンドポイントを確立し、許可リストを管理し、ユーザーの活動に基づいて、あるいは更新機能によってセッションの寿命を延長するための戦略とともに、定期的なSMS検証への依存を最小限に抑えながら、継続的なアクセスを容易にします。

このチュートリアルの このチュートリアルのコードはGitHubで入手可能です。

最新ニュースを入手するには、開発者コミュニティ コミュニティSlackにて X(旧Twitterで、そしてイベントで。

シェア:

https://a.storyblok.com/f/270183/400x400/3f6b0c045f/amanda-cavallaro.png
Amanda Cavallaroデベロッパー・アドボケイト