pauth-js で5分で実装する着信認証 — npm install 1コマンドでSMS認証を代替する
1. はじめに
着信認証API pauth.me を素のcurlで呼ぶとこうなります。
Before(SDKなし / curl):
# 1. トークン取得
TOKEN=$(curl -s -H 'api-key: test_xxx' https://pauth.me/api/v1/auth | jq -r .token)
# 2. SSEストリームで PIN を待つ(手動パース)
curl -s -N -H "Authorization: Bearer $TOKEN" \
https://pauth.me/api/v1/entry/+819012345678 | \
grep -o '"pin":"[^"]*"' | head -1
# 3. PIN照合
curl -s -H "Authorization: Bearer $TOKEN" \
"https://pauth.me/api/v1/apply?callerd_number=+819012345678&pin=1234"
After(pauth-js SDK):
import { PauthClient } from 'pauth-js';
const client = new PauthClient({ apiKey: 'test_xxx' });
const result = await client.verify('+819012345678');
console.log(result.pin); // "1234"
3エンドポイントの呼び出し、SSEストリームのバッファリング、トークン管理、エラーハンドリング——これらをSDKが全部吸収します。この記事は「pauth-jsをプロジェクトに組み込む側」の実装チュートリアルです。APIの内部設計(Asterisk/AGI/SSE)は第1弾で、予約システムへの応用は第2弾で解説しています。
2. インストール & セットアップ
# GitHubから直接インストール(npm publish準備中)
npm install github:mskz-ptplus-jp/pauth-js
APIキーの取得:
- pauth.me でサインアップ
- ダッシュボードから APIキーを発行(無料枠: 100回/月)
2種類のキー:
| キー | 用途 | 実電話発信 |
|---|---|---|
test_xxx |
開発・テスト(サンドボックス) | なし(約2秒でPIN=1234が自動返却) |
live_xxx |
本番 | あり(実際に着信が発生) |
TypeScriptプロジェクトの場合、型定義は同梱されているので @types/pauth-js は不要です。
3. 30秒クイックスタート
import { PauthClient } from 'pauth-js';
const client = new PauthClient({
apiKey: 'test_your-api-key' // サンドボックスモード(無料100回/月)
});
async function main() {
// verify() = auth() + entry() + apply() を一括実行
const result = await client.verify('+819012345678');
console.log('認証成功!', result.pin); // "1234"(サンドボックス)
}
main().catch(console.error);
実行すると約2秒後に 認証成功! 1234 と出力されます。サンドボックスモードでは実際の電話発信は不要——開発マシンから即座に動作確認できます。
verify() が内部でやっていること:
-
/api/v1/authを呼んでBearer tokenを取得 -
/api/v1/entry/{phone}にSSE接続してPIN待機 - PINが届いたら
/api/v1/applyでPIN照合 - 認証成功の結果オブジェクトを返却
4a. 実践: Node.js バックエンドでの認証フロー
Expressで電話番号認証エンドポイントを実装する例です。
// server.ts
import express from 'express';
import { PauthClient, RateLimitError, ConflictError } from 'pauth-js';
const app = express();
app.use(express.json());
const pauth = new PauthClient({
apiKey: process.env.PAUTH_API_KEY || 'test_your-api-key'
});
// POST /api/verify-phone
app.post('/api/verify-phone', async (req, res) => {
const { phoneNumber } = req.body as { phoneNumber: string };
try {
const result = await pauth.verify(phoneNumber, {
onStep: (step) => console.log(`Auth step: ${step}`)
// step: 'authenticating' → 'waiting_for_call' → 'verifying_pin'
});
res.json({ success: true, pin: result.pin });
} catch (err) {
if (err instanceof RateLimitError) {
// 同一番号への連続リクエスト制限(デフォルト: 10分に1回)
res.status(429).json({
error: 'レート制限。しばらくお待ちください',
retryAfter: err.retryAfter
});
} else if (err instanceof ConflictError) {
// 同じ番号が既に認証待ち中
res.status(409).json({ error: '認証が既に進行中です' });
} else {
res.status(500).json({ error: '認証に失敗しました' });
}
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
注意: pauth-js はサーバーサイド専用パッケージです。APIキーをクライアントに露出させないため、フロントエンドから直接呼び出してはいけません。バックエンドのAPIルートやサーバーアクションから呼ぶ構成にしてください。
エラーハンドリングの整理:
| エラークラス | HTTPステータス | 意味 | 対処 |
|---|---|---|---|
RateLimitError |
429 | レート制限超過 |
err.retryAfter 秒後に再試行 |
ConflictError |
409 | 認証進行中 | ユーザーに案内して待機 |
AuthenticationError |
401 | APIキー不正 | キーを再確認 |
PauthError(基底) |
500 | その他 | ログ記録してフォールバック |
4b. 実践: Next.js/React での認証フォーム
設計の原則: APIキーをフロントに露出させない。
Next.js App Router を使い、API Route でバックエンドを経由する構成です。
// app/api/verify/route.ts(サーバーサイド)
import { NextRequest, NextResponse } from 'next/server';
import { PauthClient } from 'pauth-js';
// PauthClient はモジュールスコープで初期化してトークンを使い回す
const client = new PauthClient({ apiKey: process.env.PAUTH_API_KEY! });
export async function POST(req: NextRequest) {
const { phoneNumber } = await req.json() as { phoneNumber: string };
try {
const result = await client.verify(phoneNumber);
return NextResponse.json({ success: true, pin: result.pin });
} catch (err) {
const status = err instanceof Error && 'statusCode' in err
? (err as { statusCode: number }).statusCode
: 500;
return NextResponse.json({ error: '認証失敗' }, { status });
}
}
// app/verify/page.tsx(クライアントサイド)
'use client';
import { useState } from 'react';
export default function VerifyPage() {
const [phone, setPhone] = useState('');
const [result, setResult] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleVerify() {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phoneNumber: phone }),
});
const data = await res.json() as { pin?: string; error?: string };
if (res.ok && data.pin) {
setResult(data.pin);
} else {
setError(data.error ?? '認証失敗');
}
} catch {
setError('ネットワークエラー');
} finally {
setLoading(false);
}
}
return (
<div>
<input
value={phone}
onChange={e => setPhone(e.target.value)}
placeholder="+819012345678"
disabled={loading}
/>
<button onClick={handleVerify} disabled={loading || !phone}>
{loading ? '認証中...' : '認証する'}
</button>
{result && <p>認証PIN: {result}</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
フロントエンドは /api/verify を叩くだけ。PAUTH_API_KEY は .env.local に保管し、process.env 経由でサーバーサイドのみアクセスします。
5. SDK内部の工夫
pauth-js が内部で解決している技術課題を紹介します(アーキテクチャの深掘りは第1弾参照)。
SSEストリームの抽象化
ブラウザの EventSource API はカスタムヘッダーが設定できないため、Authorization: Bearer xxx を渡せません。pauth-js では fetch() + ReadableStream でSSEを実装しています。
// 概念コード(実装の要点)
const response = await fetch(entryUrl, {
headers: { Authorization: `Bearer ${token}` }
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSEはチャンク分割されることがある → バッファで "\n\n" 区切りまで溜める
const lines = buffer.split('\n\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.pin) return data; // PIN到着 → 終了
}
}
}
チャンクが途中で分割されてくることへの対策として、\n\n(SSEイベント区切り)まで溜めてからJSONパースしています。
トークン自動管理
Sanctumトークンの有効期限は600秒。SDKはトークンを内部キャッシュし、残り30秒を切ったら次の呼び出し前に自動リフレッシュします。複数リクエストが並行しても、リフレッシュは1回だけ走る排他制御付きです。
エラー階層
PauthError(基底クラス)
├── AuthenticationError // 401: APIキー不正
├── ConflictError // 409: 認証進行中
└── RateLimitError // 429: レート制限(retryAfter フィールドあり)
すべてのエラーが PauthError を継承しているため、catch (err) で err instanceof PauthError という分岐だけでSDKエラーを一括捕捉できます。
6. Twilio SMS SDK との比較
同じ「電話番号で本人確認」の文脈で、Twilio Verify SDK と比べます。まず Twilio での実装コードを示します(公式ドキュメントベース)。
// twilio の例(Verify V2 API)
const twilio = require('twilio');
const client = new twilio(accountSid, authToken);
// 認証コード送信
await client.verify.v2
.services(serviceId)
.verifications.create({
to: '+819012345678',
channel: 'sms'
});
// → SMSで「Your verification code is: 123456」が届く
// コード検証
const check = await client.verify.v2
.services(serviceId)
.verificationChecks.create({
to: '+819012345678',
code: '123456'
});
console.log(check.status); // "approved" or "pending"
// pauth-js の例(同等の処理)
import { PauthClient } from 'pauth-js';
const client = new PauthClient({ apiKey: process.env.PAUTH_API_KEY! });
const result = await client.verify('+819012345678');
console.log(result.pin); // "1234"
比較表:
| 項目 | Twilio Verify (SMS) | pauth-js (着信認証) |
|---|---|---|
| インストール | npm install twilio |
npm install github:mskz-ptplus-jp/pauth-js |
| 認証実装行数 | ~20行(送信+照合+エラー) | ~5行(verify 1メソッド) |
| コスト/件 | $0.05〜0.10 USD(約¥8〜15) | $0.05 USD(約¥7) |
| 固定電話対応 | × | ○ |
| サンドボックス | 試用クレジット要 | 無料100回/月 |
| TypeScript型定義 |
@types/twilio(別途) |
同梱 |
| 外部依存(node_modules) | 多数 | 0 |
※Twilio料金は2026年2月時点の公式料金ページに基づく概算。
pauth-jsの強み:
- 固定電話対応: 高齢者・法人ユーザーが多い業態(医療・旅館など)ではカバレッジが変わる
-
依存ゼロ:
node_modulesが肥大化しない、セキュリティ監査が容易 - コスト同等以下: SMS認証より若干安く、サンドボックスが完全無料
7. まとめ
npm install github:mskz-ptplus-jp/pauth-js の1コマンドで、着信認証を verify() の1メソッドに集約できます。
-
5分で動作確認: サンドボックスモード(
test_APIキー)で実電話不要 - TypeScript対応: 型定義同梱、外部依存ゼロ
- 固定電話・携帯・IP電話すべてに対応: SMS認証では届かないユーザーをカバー
- 無料枠: 月100回まで無料(小規模サービスなら本番でも無料運用可)
関連リンク:
- GitHub: https://github.com/mskz-ptplus-jp/pauth-js
- pauth.me(APIキー発行): https://pauth.me
- APIドキュメント: https://pauth.me/docs
- 第1弾(アーキテクチャ解説): SMS認証の代替手段: 着信認証APIを個人開発した話
- 第2弾(予約ユースケース): 予約システムの無断キャンセルを着信認証で防ぐ
Discussion