Vonage × LaravelでSMS送信＆パスワードなし認証を実装してみた

# はじめに

Vonage は CPaaS（Communications Platform as a Service）のプロバイダーで、SMS送信や音声通話などのコミュニケーション機能をAPI連携で実装できるサービスを提供されています。

本記事では Vonage から提供されている API を用いて SMS送信とパスワードなし認証を実装してみました。

# 開発環境

Vonage では様々なプログラミング言語の SDK が用意されています。

https://developer.vonage.com/ja/tools

今回は PHP のフレームワークである Laravel を用いて実装します。

* PHP v8.2
* Laravel v10.x

:::note info
Vonage API を用いた実装方法を確認することが目的であるため、本記事では画面を作成せずに Artisan コマンドを作成し、簡略化を図っております。
:::

Laravel で Vonage を使用するためのパッケージとして、Vonage のサイトで紹介されている `vonage/vonage-laravel` と Laravelの公式マニュアルで紹介されている `laravel/vonage-notification-channel` があります。

https://github.com/vonage/vonage-laravel

https://github.com/laravel/vonage-notification-channel

今回は Laravel の公式マニュアルに記載されている `laravel/vonage-notification-channel` を使用して進めていきます。

ちなみに、どちらを用いても SMS 送信やパスワードなし認証は実装可能です。

# 使用準備

Laravel の公式マニュアルに従って、Vonage を使用するための準備を進めていきます。

https://laravel.com/docs/10.x/notifications#sms-notifications

まずは以下のコマンドを実行して、composer でパッケージをインストールします。
```bash
composer require laravel/vonage-notification-channel guzzlehttp/guzzle
```

インストールが完了したら、`.env` に Vonage を使用するために必要な情報を記述します。

```:.env
VONAGE_KEY=[VonageのDashboardで取得したAPI key]
VONAGE_SECRET=[VonageのDashboardで取得したAPI Secret]
```

これだけで準備完了です。

# SMS送信

入力されたメッセージを送信する Artisan コマンドを作成して、SMS送信の動作を確認してみます。

こちらの実装では、Vonage の **SMS API** を使用しています。

https://developer.vonage.com/ja/messaging/sms/overview

## Notifications を使用して送信する方法

Laravel の公式マニュアルには Notifications（通知）の機能を活用してSMS送信を行う方法が記載されています。

まずは `VonageNotification.php` を作成します。

```php:app\Notifications\VonageNotification.php
class VonageNotification extends Notification
{
    use Queueable;

    private string $message;

    public function __construct(string $message = '')
    {
        $this->message = $message;
    }

    public function via(object $notifiable): array
    {
        return ['vonage'];
    }

    public function toVonage(object $notifiable): VonageMessage
    {
        $from = 'Laravel App';  // 送信先で表示される送信者ID
        return (new VonageMessage)->from($from)->content($this->message);
    }
}
```

次に、作成した `VonageNotification.php` を用いてSMS送信を行う Artisan コマンドを作成します。

```php:app\Console\Commands\SendSms.php
class SendSms extends Command
{
    protected $signature = 'send:sms';

    public function handle()
    {
        $message = $this->ask('What message do you want to send?');

        $to = '送信先の電話番号';
        Notification::route('vonage', $to)->notify(new VonageNotification($message));
    }
}
```

それでは実際にコマンドを実行して、動作確認を行ってみます。`$to` の値を自分の電話番号に置き換えてから実行してください。

```bash
php artisan send:sms

What message do you want to send?:
> Hello world!
```

スマホで確認したところ、入力したメッセージが届いていました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/fa9788d7-e3c4-ae2f-24a8-1a0d91dc9c68.png" width="350" />

また、標準のままだと日本語が文字化けしてしまうため、日本語を送信するためには `unicode()` を実行する必要があります。

```diff_php:app\Notifications\VonageNotification.php
    public function toVonage(object $notifiable): VonageMessage
    {
-       return (new VonageMessage)->from($from)->content($this->message);
+       return (new VonageMessage)->from($from)->content($this->message)->unicode();       
    }
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/d00232bf-6ed7-8809-d9b8-e73848355b36.png" width="350" />


## Vonage ファサードを用いて送信する方法

`vonage/vonage-laravel` の [README](https://github.com/Vonage/vonage-laravel/blob/main/README.md#usage) に記載されている方法で、シンプルな記述でSMS送信を行うことができます。

```php
class SendSms extends Command
{
    protected $signature = 'send:sms';

    public function handle()
    {
        $message = $this->ask('What message do you want to send?');

        $to = '送信先の電話番号';
        $from = 'Laravel App';  // 送信先で表示される送信者ID
        Vonage::sms()->send(new SMS($to, $from, $message));
    }
}
```

# パスワードなし認証

SMS／音声／メールを用いたパスワードなし認証を行う Artisan コマンドを作成して、動作を確認してみます。

こちらの機能では、Vonage の **Verify API** を使用しています。

https://developer.vonage.com/ja/verify/overview

マニュアルによると、旧バージョンの Verify V1 と現バージョンの Verify V2 が提供されているようです。

## Verify V1

まずは旧バージョンを用いて実装してみます。V1 では、SMSと音声（読み上げ）による組み合わせで認証を行うことが可能です。デフォルトのワークフロー（workflow_id = 1）では SMS → 音声 → 音声の流れで認証が行われます。

1. ワークフロー開始
2. SMS で PIN コードを送信
 ~ 数分間経過 ~
3. 電話が掛かってきて PIN コードを読み上げ
 ~ 数分間経過 ~
4. 電話が掛かってきて PIN コードを読み上げ
 ~ 数分間経過 ~
4. ワークフロー終了


ちなみにワークフローは 7 種類あり、workflow_id を指定すれば選択可能です。

```php:選択可能なワークフロー
public const WORKFLOW_SMS_TTS_TSS = 1;  // SMS → 音声 → 音声（デフォルト）
public const WORKFLOW_SMS_SMS_TSS = 2;  // SMS → SMS → 音声
public const WORKFLOW_TTS_TSS = 3;      // 音声 → 音声
public const WORKFLOW_SMS_SMS = 4;      // SMS → SMS
public const WORKFLOW_SMS_TTS = 5;      // SMS → 音声
public const WORKFLOW_SMS = 6;          // SMS
public const WORKFLOW_TTS = 7;          // 音声
```

また、発行する PIN コードの長さは 4桁 または 6桁 を選択可能です。

```php:選択可能な PIN コード桁数
public const PIN_LENGTH_4 = 4;
public const PIN_LENGTH_6 = 6;
```

それではデフォルトのワークフロー（SMS → 音声 → 音声）でパスワードなし認証を行う Artisan コマンドを作成して実行してみます。ちなみに、PINコードは 6桁 を指定しています。

:::note warn
発行した PIN コードに対しては 3 回までチェックが可能です。3 回失敗すると PIN コードが無効となり、ワークフローが終了します。
:::

```php:app\Console\Commands\VerifyV1.php
class VerifyV1 extends Command
{
    protected $signature = 'verify:v1';

    public function handle()
    {
        $to = '送信先の電話番号';
        $from = 'Sender ID';
        $brand = 'Brand Name';

        // 認証開始
        $workflowId = \Vonage\Verify\Request::WORKFLOW_SMS_TTS_TSS;
        $request = new \Vonage\Verify\Request($to, $brand, $workflowId);
        $request->setCodeLength(\Vonage\Verify\Request::PIN_LENGTH_6);
        $request->setLocale('ja-jp');
        $request->setSenderId($from);
        $response = Vonage::verify()->start($request);

        $failedCount = 0;
        while ($failedCount < 3) {
            $code = $this->ask('Please enter the verification code');
            try {
                // PIN コードのチェック
                $result = Vonage::verify()->check($response->getRequestId(), $code);
                $responseData = $result->getResponseData();
                if ($responseData['status'] === "0") {
                    break;
                }
            } catch (\Vonage\Client\Exception\Exception $e) {
                $failedCount++;
                $this->error($e->getMessage());
            }
        }
        // 結果メッセージの表示
        if ($failedCount < 3) {
            $this->info('Successfully authenticated');
        } else {
            $this->error('Authentication has failed');
        }
    }
}
```

### 認証成功の場合

Artisan コマンドを実行すると、スマホに SMS が送信されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/cb7531fb-50ee-07f0-6308-9694bc905aa0.png" width="350" />

受信した PIN コードを入力すると、認証成功のメッセージが表示されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/c5faf767-4115-ece5-04ab-5acfe011a301.png" width="350" />

### 認証失敗の場合

誤った PIN コードを3回入力して失敗すると、ワークフローが終了となり、認証失敗のメッセージが表示されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/4b3b4629-475f-f424-60c1-a50e52bd2bcc.png" width="450" />

## Verify V2

次に現バージョンを用いて実装してみます。V2 では独自のワークフローを柔軟に組み上げることが可能です。使用できるチャンネルは以下の通りです。

* SMS
* 音声
* WhatsApp
* WhatsApp Interactive ／ WhatsApp Codeless
* 電子メール
* サイレント認証（SIM を用いた認証）

今回は、SMS → 電子メール → 音声 のワークフローでパスワードなし認証を行う Artisan コマンドを作成して実行してみます。ちなみに、PINコードの桁数も 4桁 ～ 10桁 で柔軟に選択可能となりました。

:::note warn
記事投稿時点では Verify V2 は日本語に対応しておりません。
:::

```php:app\Console\Commands\VerifyV2.php
class VerifyV2 extends Command
{
    protected $signature = 'verify:v2';

    public function handle()
    {
        $toPhoneNumber = '送信先の電話番号';
        $toEmail = '送信先のメールアドレス';
        $brand = 'Laravel App';

        // 認証開始
        $request = new SMSRequest($toPhoneNumber, $brand);
        $request->setLength(10);    // PIN コードの桁数を指定
        $request->setTimeout(60);   // PIN コードの有効期限を指定
        $emailWorkflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_EMAIL, $toEmail);
        $request->addWorkflow($emailWorkflow);
        $voiceWorkflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_VOICE, $toPhoneNumber);
        $request->addWorkflow($voiceWorkflow);
        $response = Vonage::verify2()->startVerification($request);

        $isAuthenticated = false;
        while (true) {
            $code = $this->ask('Please enter the verification code (Enter "no" to exit)');
            if ($code === 'no') {
                try {
                    // ワークフローのキャンセル
                    Vonage::verify2()->cancelRequest($response['request_id']);
                } catch (\Exception $e) {
                    $this->error($e->getMessage());
                }
                break;
            }
            // PIN コードのチェック
            try {
                $result = Vonage::verify2()->check($response['request_id'], $code);
                if ($result === true) {
                    $isAuthenticated = true;
                    break;
                }
            } catch (\Exception $e) {
                $this->error($e->getMessage());
            }
        }
        // 結果メッセージの表示
        if ($isAuthenticated) {
            $this->info('Successfully authenticated');
        } else {
            $this->error('Authentication has failed');
        }
    }
}
```

### 認証成功の場合

Artisan コマンドを実行すると、スマホに SMS が送信されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/b6b96eb0-1781-550e-2351-5a231932c91d.png" width="350" />

そのまま何もしないで SMS に記載された PIN コードの有効期限が過ぎると、メールが届きました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/74d24a7f-7af0-47eb-5125-9fbd29366f6d.png" width="500" />

そのまま何もしないでメールに記載された PIN コードの有効期限が過ぎると、スマホに着信がありました。読み上げられた PIN コードを入力すると、認証成功のメッセージが表示されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/5cd5f612-0665-eb49-4609-3d3015c478bd.png" width="300" />

### 認証失敗の場合

誤った PIN コードを入力するとエラーが表示されました。また、"no" と入力するとワークフローが終了となり、認証失敗のメッセージが表示されました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339770/16563f33-1cb5-aafa-488d-74f0a2b968f6.png" width="450" />

# おわりに

Vonage の SMS API と Verify API を用いて、SMS 送信とパスワードなし認証を実装してみました。

割と簡単に実装することができるので、こういった機能を求められた場合は CPaaS の選択肢の1つとして Vonage を検討してみてはいかがでしょうか？

また、本記事では紹介しきれなかった音声通話（Voice API）などを Laravel（PHP）で実装する場合は、`vonage/vonage-php-sdk-core` の [README](https://github.com/Vonage/vonage-php-sdk-core/blob/main/README.md) を参考にされると良いと思います。