概要
本記事では、VonageAPIのVerify APIとSMS APIを用いて、電話番号認証をして認証済み電話番号に対してSMSでメッセージを送信するWebアプリケーションを作成するハンズオン記事になっています。
MFA(多要素認証)は今やアプリケーションのセキュリティを向上させる上で重要な役割を担っています。各個人でMFAのための認証基盤を作成するのはコスト的にも技術的にも大変だと思います。VoyageAPIを使用することで認証方法の1つである電話番号認証が簡単に実装できることがわかりました。
本記事では、まずVonageが提供している Verify APIを用いて SPAによる電話番号認証を作成していきます。
(おまけ) セクションで、この電話番号認証をより堅牢にするために Number Insight API を用いて電話番号の有効性チェックの導入も行います。
そして、認証後の電話番号とSMS APIを用いて認証済みの電話番号端末に対して任意のメッセージでSMSを送信機能まで作成します。
以下のような画面構成を持つSPAのWebアプリケーションを作成していきます。
ホーム | 認証コード発行 | 認証 |
---|---|---|
作成するアプリケーションの全体像です。 緑の四角がSPAのページを示していて、赤の四角がバックエンドサーバーの機能を示しています。直線がSPA内でのページの遷移、破線がそれぞれのAPIへのリクエストの流れを示しています。
作成したコードはこちらに置いてありますので参考にしてください。
それではアプリケーションを作成していきましょう。
アプリケーションを作成する
Vonageアカウントの作成する
Vonageアカウントの作成をしておきます。
プロジェクトを作成する
Reactプロジェクトの作成(フロントエンド)
$ npx create-react-app vonage-app
必要なパッケージインストール
アプリケーションを完成させるために必要なパッケージをインストールしていきます。
$ npm install express express-session body-parser dotenv @vonage/server-sdk
パッケージ名 | 内容 |
---|---|
express | Webアプリケーションフレームワーク |
express-session | ユーザーのログイン状態を管理する |
body-parser | POSTリクエストをパースする |
dotenv | VonageAPI KEY, SECRET, BRAND NAME(アプリケーション名)を環境変数管理 |
@vonage/server-sdk | VonageAPIを扱うためのSDK |
server.jsのフレームを作成(バックエンド)
require('dotenv').config();
const path = require('path')
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
const { Vonage } = require('@vonage/server-sdk');
const VONAGE_API_KEY = process.env.VONAGE_API_KEY;
const VONAGE_API_SECRET = process.env.VONAGE_API_SECRET;
const VONAGE_BRAND_NAME = process.env.VONAGE_BRAND_NAME;
let verifyRequestId = null;
let verifyRequestNumber = null;
// Location of the application's CSS files
app.use(express.static('build'));
// The session object we will use to manage the user's login state
app.use(session({
secret: 'loadsofrandomstuff',
resave: false,
saveUninitialized: true
}));
app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Define your routes here
// Run the web server
const server = app.listen(3000, () => {
console.log(`Server running on port ${server.address().port}`);
});
dotenvで.env
ファイルによって、環境変数が使用できるようになっているので.env
ファイルにVONAGE_API_KEY、VONAGE_API_SECRETをVonageAPIDeveloperページから取得して環境変数に追加します。VONAGE_BRAND_NAMEは任意で適当な値を設定できます。
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
VONAGE_BRAND_NAME=<アプリケーション名>
package.jsonにexpressサーバー起動ようのスクリプトを追加していきます。
// ...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"server": "node server.js" // ← 追加
},
// ...
アプリを確認する
ここまで来たところで、現在のアプリケーションを確認してみましょう
以下のコマンドを実行して、Reactアプリケーションをビルドし、expressサーバーを立ち上げてブラウザで確認します。
$ npm run build // ← Reactアプリケーションのビルド
$ npm run server // ← Expressサーバー起動
> vonage-app@0.1.0 server
> node server.js
Server running on port 3000
認証コードを取得する
画面UIを作成する(フロントエンド)
必要なページを作成していくためにReactRouter、UIを作成するためにMUIをインストールします。
$ npm install --save react-router-dom
$ npm install --save @mui/material @emotion/react @emotion/styled
各ページを作成する
以下それぞれのファイルのコードを記載しています。
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Home from './pages/Home';
import Authenticate from './pages/Authenticate';
import { Container } from '@mui/system';
function App() {
return (
<div className="App">
<Container maxWidth="sm">
<BrowserRouter>
<Routes>
<Route path={"/"} element={<Home />} />
<Route path={"/authenticate"} element={<Authenticate />} />
</Routes>
</BrowserRouter>
</Container>
</div>
);
}
export default App;
import { Button } from "@mui/material";
import { Link } from "react-router-dom";
const Home = () => {
return (
<>
<h1>Account Management!</h1>
<p>
You have verified your identity using the phone number TODO
and are now permitted to make changes to your account.
</p>
<Link to={"/authenticate"}>
<Button variant="contained">
Verify me
</Button>
</Link>
</>
);
};
export default Home;
import { Button, FormControl, TextField } from "@mui/material";
import { useState } from "react";
const Authenticate = () => {
const [mobileNumber, setMobileNumber] = useState("");
const handleChangeMobileNumber = (e) => {
setMobileNumber(e.target.value);
}
const requestVerificationCode = async () => {
const formatMobileNumber = "81" + mobileNumber.slice(1);
const response = await fetch("/verify", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ number: formatMobileNumber })
})
console.log(response.json());
}
return (
<>
<h1>Authenticate</h1>
<FormControl fullWidth sx={{ m: 1 }}>
<TextField
id="outlined-name"
label="Mobile number"
inputProps={{ inputMode: "numeric", pattern: '[0-9]*' }}
onChange={handleChangeMobileNumber}
/>
</FormControl>
<FormControl fullWidth sx={{ m: 1 }}>
<Button
variant="contained"
onClick={requestVerificationCode}
>
Get Verification Code
</Button>
</FormControl>
<h2>{mobileNumber}</h2>
</>
);
};
export default Authenticate;
ここまで完成すると以下のような画面が作成できました。
ホーム画面 | 認証コードリクエスト画面 |
---|---|
認証コード発行をVonageにリクエストするエンドポイントを作成(バックエンド)
/verify
というPOSTリクエストのパスを作成し、リクエストで取得した携帯電話番号をvonageインスタンスにセットして認証コードをリクエストします。詰まってしまう箇所として、リクエストする時の電話番号は国際電話番号の形式にする必要があることに注意です。
09012345678
であれば、先頭のゼロを取り除き、日本を表す「81」を付けた 819012345678
となります。
// Define your routes here
app.post('/verify', (req, res) => {
verifyRequestNumber = req.body.number;
vonage.verify.start({
number: verifyRequestNumber,
brand: VONAGE_BRAND_NAME
}).then((resp) => {
console.log(resp)
verifyRequestId = resp.request_id;
console.log(`request_id: ${verifyRequestId}`);
res.status(201).send("success");
}).catch(err => console.error(err));
})
以下のコードは画面側で /verify
へリクエストする際のイベント関数ですが、リクエストの前に電話番号を国際電話番号形式に変換するロジックを入れています。(※ 画面UIを作成する(フロントエンド)章の pages/Authenticate.jsx
に含まれるコードの一部です。)
// ...
const requestVerificationCode = async () => {
const formatMobileNumber = "81" + mobileNumber.slice(1);
const response = await fetch("/verify", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ number: formatMobileNumber })
})
console.log(response.json());
}
// ...
認証情報が携帯電話に送信されるかを確認する
認証コードリクエスト画面で、実際に自身の携帯電話番号を入力して、GET VERIFICATION CODE
ボタンを押下してみましょう。
するとExpressサーバー側のログにはrequest_idが表示され、正常にリクエストできていることが確認できます。
{ request_id: 'cec9cb43b5434d12862b3b867cfa4da7', status: '0' }
request_id: cec9cb43b5434d12862b3b867cfa4da7
実際に携帯電話にもSMS通知で認証番号が届いていることがわかります。
認証コード |
---|
認証コードを認証する
画面UIを作成する(フロントエンド)
新規で認証コードを入力するためのページを作成します。
ページに必要なUIとしては、携帯電話に送信された認証番号を入力するテキストフィールドとリクエストを送信するためのボタンなのでほとんどAuthenticate
ページと変わらないです。
以下がVerifyCode
ページの全体のソースコードです。
import { Button, FormControl, TextField } from "@mui/material";
import { useState } from "react";
const VerifyCode = () => {
const [code, setCode] = useState("");
const handleChangeCode = (e) => {
setCode(e.target.value);
}
const requestVerificationCode = async () => {
const response = await fetch("/check-code", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
})
console.log(response.json());
}
return (
<>
<h1>VerifyCode</h1>
<FormControl fullWidth sx={{ m: 1 }}>
<TextField
id="outlined-name"
label="Code"
inputProps={{ inputMode: "numeric", pattern: '[0-9]*' }}
onChange={handleChangeCode}
/>
</FormControl>
<FormControl fullWidth sx={{ m: 1 }}>
<Button
variant="contained"
onClick={requestVerificationCode}
>
Verify me!
</Button>
</FormControl>
</>
);
};
export default VerifyCode;
認証ページ |
---|
新規ページに遷移できるようにするために、App.jsx
を修正します。
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Home from './pages/Home';
import Authenticate from './pages/Authenticate';
import { Container } from '@mui/system';
import VerifyCode from './pages/VerifyCode';
function App() {
return (
<div className="App">
<Container maxWidth="sm">
<BrowserRouter>
<Routes>
<Route path={"/"} element={<Home />} />
<Route path={"/authenticate"} element={<Authenticate />} />
<Route path={"/verifycode"} element={<VerifyCode />} />
</Routes>
</BrowserRouter>
</Container>
</div>
);
}
export default App;
また、Authenticate
ページで認証コード発行のリクエスト後にページ遷移させるために、ReactRouterのuseNavigate()
を利用してVerifyCode
ページに遷移するように修正します。
import { Button, FormControl, TextField } from "@mui/material";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const Authenticate = () => {
const navigate = useNavigate();
const [mobileNumber, setMobileNumber] = useState("");
const handleChangeMobileNumber = (e) => {
setMobileNumber(e.target.value);
}
const requestVerificationCode = async () => {
const formatMobileNumber = "81" + mobileNumber.slice(1);
await fetch("/verify", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ number: formatMobileNumber })
});
navigate("/verifycode")
}
return (
<>
<h1>Authenticate</h1>
<FormControl fullWidth sx={{ m: 1 }}>
<TextField
id="outlined-name"
label="Mobile number"
inputProps={{ inputMode: "numeric", pattern: '[0-9]*' }}
onChange={handleChangeMobileNumber}
/>
</FormControl>
<FormControl fullWidth sx={{ m: 1 }}>
<Button
variant="contained"
onClick={requestVerificationCode}
>
Get Verification Code
</Button>
</FormControl>
<h2>{mobileNumber}</h2>
</>
);
};
export default Authenticate;
認証コード認証をVonageにリクエストするエンドポイント作成(バックエンド)
/verify
の下に新しく/check-code
のエンドポインを立て、認証をリクエストする処理を書いて行きます。発行のコードと似ていて、vonageインスタンスに発行の際に取得したverifyRequestId
と画面側からのリクエストで取得する携帯電話に送信された認証番号であるcode
を引数にいれて認証をリクエストします。
app.post('/check-code', (req, res) => {
vonage.verify.check({
request_id: verifyRequestId,
code: req.body.code
}).then((resp) => {
console.log(resp)
if (resp.status === 0) {
req.session.user = {
number: verifyRequestNumber
};
}
res.status(201).send("success");
}).catch(err => console.error(err));
});
認証の確認
Expressサーバーで認証コードが正常に受理されると以下のようなレスポンスが返却され console.logによってターミナルに表示されます。
status: '0'
が成功のステータスレスポンスです。
{ code: '3904' }
{
request_id: 'c753cc99fb714c80ad3656ac364e1579',
status: '0',
event_id: '330000005912A562',
price: '0.05000000',
currency: 'EUR',
estimated_price_messages_sent: '0.07000000'
}
認証状態を表示する
ホーム画面を修正する(フロントエンド)
Home.jsx
に認証状態を取得する処理を追加していきます。画面レンダリングされたと同時に取得したいのでuseEffect
でAPIコールを行います。
また、認証状態である場合には認証済みの電話番号を表示させたいので、表示させるUIデザインを少し可愛くしていきます。そのために@mui/icons-material
をインストールします。
$ npm install --save @mui/icons-material
以下がHome.jsxの最新の全体コードです。
import { Button, Chip } from "@mui/material";
import { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import MdPhone from '@mui/icons-material/Phone';
import { Box } from "@mui/system";
const Home = () => {
const [verifyNumber, setVerifyNumber] = useState(null);
const [fromVerifyCode, setFromVerifyCode] = useState(null);
const search = useLocation().search;
const query = new URLSearchParams(search);
if (query.verify) {
setFromVerifyCode(query.verify);
}
useEffect(() => {
const getVerify = async () => {
const response = await fetch("/verify-number", {
method: "GET",
headers: {
'Content-Type': 'application/json'
},
})
const json = await response.json();
setVerifyNumber(json.verifyNumber);
}
getVerify();
}, [fromVerifyCode]);
const phoneLabelMessage = verifyNumber ? `+${verifyNumber}` : "You are not verified";
return (
<>
<h1>Account Management!</h1>
<Box sx={{ m: 1 }}>
<Chip icon={<MdPhone />} label={phoneLabelMessage}/>
</Box>
<Link to={"/authenticate"}>
<Button variant="contained">
Verify me
</Button>
</Link>
</>
);
};
export default Home;
認証状態を取得するエンドポイントを作成(バックエンド)
/check-code
のエンドポイントの下に新しく、認証状態を取得するGETリクエスト用のエンドポイント /verify-number
を作成します。ここでは、認証済みであれば verifyNumber
に認証された電話番号が入り、未認証であれば、null
が入るようになっています。
app.get('/verify-number', (req, res) => {
console.log(req.session.user)
res.status(200).send({
verifyNumber: req?.session?.user?.number,
})
});
認証状態の確認
未認証 | 認証済み |
---|---|
これでSPAに電話番号認証を導入することができます。VonageAPIを使用するとめちゃくちゃ簡単に作成できますね!
認証した電話番号にSMSを送信する機能
最後に、認証した電話番号を用いてSMSを送信する機能を作成していきます。
ホーム画面を修正する(フロントエンド)
Home.jsx
に認証済みの電話番号があれば、テキストボックスとSMSの送信ボタンを表示させるようにJSX部分に追加していきます。
また、SMSで送信したい内容用のテキストボックスのchangeイベントに対応する handleChangeText
関数とSMSを送信するためのhandleSubmitSms
関数を定義して、それぞれのイベントに設定します。SMSの送信のためのエンドポイントのAPIは後述のバックエンド側の処理で書いていきますが、send-sms
にPOSTでリクエストするようにします。
import { Button, Chip, FormControl, TextField } from "@mui/material";
import { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import MdPhone from '@mui/icons-material/Phone';
import { Box } from "@mui/system";
const Home = () => {
const [verifyNumber, setVerifyNumber] = useState(null);
const [fromVerifyCode, setFromVerifyCode] = useState(null);
const [text, setText] = useState("");
const search = useLocation().search;
const query = new URLSearchParams(search);
if (query.verify) {
setFromVerifyCode(query.verify);
}
useEffect(() => {
const getVerify = async () => {
const response = await fetch("/verify-number", {
method: "GET",
headers: {
'Content-Type': 'application/json'
},
})
const json = await response.json();
setVerifyNumber(json.verifyNumber);
}
getVerify();
}, [fromVerifyCode]);
const phoneLabelMessage = verifyNumber ? `+${verifyNumber}` : "You are not verified";
const handleChangeText = (e) => {
setText(e.target.value);
}
const handleSubmitSms = async () => {
const data = { number: verifyNumber, text };
await fetch("/send-sms", {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
}
return (
<>
<h1>Account Management!</h1>
<Box sx={{ m: 1 }}>
<Chip icon={<MdPhone />} label={phoneLabelMessage}/>
</Box>
<Link to={"/authenticate"}>
<Button variant="contained">
Verify me
</Button>
</Link>
{ verifyNumber &&
(<Box sx={{ m: 2 }}>
<FormControl fullWidth sx={{ m: 1 }}>
<TextField
id="outlined-name"
label="sms text"
onChange={handleChangeText}
/>
</FormControl>
<FormControl fullWidth sx={{ m: 1 }}>
<Button
variant="contained"
onClick={handleSubmitSms}
>
Send SMS
</Button>
</FormControl>
</Box>)
}
</>
);
};
export default Home;
SMSを送信するためのエンドポイントを作成(バックエンド)
/verify-number
のエンドポイントの下に/send-sms
の新規エンドポイントを作成します。ここでは、VonageSDKのSMS APIを使用していきますが、to、from、textさえ指定すれば簡単に任意のメッセージをSMSで送信することができます。
任意のメッセージは画面からリクエストされて送られてくるreq.body.text
を設定します。
app.post('/send-sms', async (req, res) => {
const to = req.body.number;
const text = req.body.text;
await vonage.sms.send({ to, from: VONAGE_BRAND_NAME, text })
.then(resp => {
console.log('Message sent successfully');
console.log(resp);
})
.catch(err => console.error(err));
res.status(201).send("success");
});
SMS送信の確認
以下のコマンドで再度フロントエンドをビルドして、サーバーを立ち上げます。
$ npm run build
$ npm run server
立ち上げたWebアプリケーションで電話番号認証をすると、以下のように新規で追加したテキストボックスとSEND SMS
ボタンが表示されます。ここに任意のメッセージを入れ、送信!
そうすると左のSMSメッセージ画像のようにアプリブランド名とメッセージがSMSで送信されました!
電話番号認証済みHOME画面 | SMSメッセージ |
---|---|
バックエンドのログも確認しておきましょう。
console.logに仕込んでおいた Message sent successfully
が表示されて、VonageAPIからのレスポンスが表示されていますね。
Message sent successfully
{
messageCount: 1,
'message-count': '1',
messages: [
{
to: 'XXXXXXXXXX', ← 電話番号なのでマスク
'message-id': 'e9cfde87-6e96-476c-a37b-9c0cfbe90eb1',
status: '0',
'remaining-balance': '10.59800000',
'message-price': '0.07000000',
network: '44007',
messageId: 'e9cfde87-6e96-476c-a37b-9c0cfbe90eb1',
remainingBalance: '10.59800000',
messagePrice: '0.07000000'
}
]
}
SMS送信も驚くほど簡単に実装できました。
(おまけ) 電話番号認証を少し堅牢にする
VonageAPIにはNumber Insight APIという電話番号の有効性や到達性をチェックするためのAPIがあります。このAPIを用いて、電話番号認証のロジックを少し改良します。
Expressサーバーの /verify
エンドポイントで電話番号に対して認証コードを送信する前に、その電話番号が有効化どうかをNumber Insight APIによってチェックします。
statusが0
であれば、正常な電話番号として処理されているので、それ以外であればhttp statusコード400を返すようにします。
app.post('/verify', async (req, res) => {
verifyRequestNumber = req.body.number;
+ const resp = await vonage.numberInsights.basicLookup(req.body.number);
+ console.log(resp);
+ if (resp.status !== 0) {
+ return res.status(400).send("invalid number");
+ }
vonage.verify.start({
number: verifyRequestNumber,
brand: VONAGE_BRAND_NAME
}).then((resp) => {
console.log(resp)
verifyRequestId = resp.request_id;
console.log(`request_id: ${verifyRequestId}`);
res.status(201).send("success");
}).catch(err => console.error(err));
});
これで有効性チェックも導入できました。
ちなみに、Number InsightAPIのレスポンスは以下のような中身でした。国や国際電話番号プレフィックスもわかるので認証APIを国際化もできそうですね。
{
status: 0,
status_message: 'Success',
request_id: 'e1dfd728-7ce0-44ba-ad40-e3f177892492',
international_format_number: 'XXXXXXXX', ← マスク
national_format_number: 'XXX-XXXX-XXXX', ← マスク
country_code: 'JP',
country_code_iso3: 'JPN',
country_name: 'Japan',
country_prefix: '81'
}
トラブルシューティング
もしAPIエラー等で悩まされる場合はAPIエラーがまとめられたドキュメントがあるので参照してみるとヒントが得られるかもしれないです。
筆者はデバッグしている際によく status: 10
が返却されて、セッション切れを待つことがありました笑。(他にもverifyすると解消されます。)
Code | Text | Definition |
---|---|---|
10 | Concurrent verifications to the same number are not allowed | Two simultaneous requests to the same number are not allowed. The customer can perform a verification request to the same number only after the previous request to that number has been marked as SUCCESSFUL, FAILED, or EXPIRED. |
API Reference
また今回利用したAPIはOpenAPIのような形式でRESTful APIのインターフェースが定義されている仕様書まであるので、ぜひ参照して見てください。
- https://developer.vonage.com/api/verify
- https://developer.vonage.com/api/sms
- https://developer.vonage.com/api/number-insight
VonageAPIでの開発に欲しかった機能
開発時点では、ダミーの電話番号や認証コードが1234などで通るようなsandbox機能があるといいなぁと思いました。しかし、Dashboardも見やすく各APIの実装例も豊富なので困らずに実装できました。
最後に
今回初めてVonageAPIを触ってみましたが、ドキュメントが充実しておりとても使いやすいサービスでした。SDKの更新に対応できていないドキュメントなどもありましたが、Dashboardから見れる説明はほぼ新しい情報だったので特に困らず実装をすることができました。
また、カレンダーに参加するにあたって無料のテストクレジットや追加のクレジットクーポンも提供してくださっていて大変助かりました。他のVoyageAPIも興味深いものが多いのでこの機会を活用して色々試して見ようと思いました。ありがとうございました。
参考