【Vonage】Vonage SDKを使ってWebアプリで電話番号認証を実装【Go】

この記事は [VonageコミュニケーションAPIを使ってみた、Vonageのことなら何でも共有しよう！ by Vonage Advent Calendar 2022](https://qiita.com/advent-calendar/2022/vonage) に参加しています。

## はじめに
私は昨年、Vonageさんのアドベントカレンダーを見かけて　↓ のような記事で参加させてもらいました。GoのSDKが見当たらなかったので作ってみる！という試みでしたが、終盤で「あれ、まだ大々的に紹介されてないけどGoのSDK開発中ですね！」と気づくという、ちょっと残念な結果に :sweat: 

https://qiita.com/tenkoh88/items/6e97f28bc9dbb37a2e36

今回はそのリベンジの位置付けで、Vonageさんの`Verify API`をSDKを活用して使用する方法を、シンプルなWebアプリケーション例を通じて試してみました。

## この記事の内容
- おさらい：Vonageから提供されているサービス
- 電話番号を使った多要素認証の流れ
- GoによるWebアプリケーションでの電話番号認証サンプル

## こんな人におすすめの記事です
- CPaaSを使ってみたい方
- 電話番号を使った多要素認証の流れを理解したい方
- なんでもGo言語で書きたい方

## おさらい：Vonageから提供されているサービス
VonageはCPaaS(Communication Platform as a Service)の1つで、企業と顧客のコミュニケーションだったり、従業員同士のコミュニケーションを支える様々なサービスを提供しています。

CPaaSって何？Vonageではどのようなことができるの？については昨年のアドベントカレンダー記事が大変分かりやすいです。

https://qiita.com/nishina555/items/36600e986ec0040c0bda

上記の記事から年も跨いだことなので、一応公式サイトのトップページから、現在提供されているサービス一覧も確認してみましょう。

https://www.vonagebusiness.jp/?bypassgeoloc=true

今回はこの中から **コミュニケーションAPI** に焦点を当ててみます。（個人で触ってみるレベルのサービスはコミュニケーションAPIぐらいしか無いと思いましたので。。。）

コミュニケーションAPIはさらにVoice API, Video API, Message APIの3つに分けられています。昨年に続き、Message APIの中から、 **簡単に多要素認証を導入可能な`Verify API`を使ってみたいと思います。**

https://www.vonagebusiness.jp/communications-apis/?icmp=megamenu|mainnav_製品_gotothecommunicationsapispage_overview_novalue

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

## 電話番号を使った多要素認証の流れ
上記ドキュメントにも記載されていますが、ベーシックな流れは次のとおりです。

```plantuml
ユーザー -> Webサービス: 電話番号送信
Webサービス -> Vonage: 電話番号認証の開始
Vonage -> Webサービス: 認証リクエストIDの返答
Vonage -> ユーザー: SMSや音声案内でPINコード送信
ユーザー -> Webサービス: PINコード送信
Webサービス -> Vonage: 認証リクエストIDとPINを送信
Vonage -> Webサービス: 認証結果を返答
```

上図における「電話番号認証の開始」や「認証リクエストIDとPINを送信」など、WebサービスがVonageに対して行うリクエストの例は　↓ に多様な例が紹介されています。助かりますね。

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

ただ実際には、リクエストIDをどうやってWebサービス側で保持しておくのか、など、少し考えてしまうこともあるのではないでしょうか？その疑問に対しては次の例が大変参考になります。Node.jsの場合について、認証開始から完了までの一連の流れをサンプルアプリケーションを交えて解説してくれています。

https://developer.vonage.com/blog/2018/05/10/nexmo-verify-api-implementation-guide-dr

さて、しかしながら**私はGo言語で書きたいところです（ただの趣味）**。そこで活躍するのがVonageから提供されているGo SDKですね。

https://github.com/Vonage/vonage-go-sdk

:::note info
ただ、最近はあまり活発に開発が行われていないのでしょうか？今回サンプルアプリケーションを作る中でSDKに対してバグ修正のプルリクを出したのですがなかなかマージされません。お願いしますね！
:::

コマンドラインで使う簡易なアプリ例は上記リポジトリに掲載されています。これとNode.jsの例を参考に電話番号認証をする簡単なWebアプリケーションサンプルを構築してみたいと思います。

## GoによるWebアプリケーションでの電話番号認証サンプル

### 成果物イメージ
作成するWebアプリケーションの概略は次のとおりです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1553191/299ccce4-1e04-001b-9882-d1331b57d5b6.png)

1. ユーザーがWebアプリにアクセスしたとき、電話番号の認証が済んでいない場合には、電話番号入力フォームが表示されます。
1. 認証を開始するとユーザーにはPINコード入力画面が表示されます。このとき、WebアプリはVonageに認証リクエストを送信しており、ユーザーにはVonageからSMSを通じてPINコードが送信されます。
1. ユーザーがPINコードを用いて認証を行うとトップページにリダイレクトされます。電話番号認証が済んでいることをWebアプリ内の状態として保持しているため、最初とは異なるメッセージが表示されます。

:::note info
実際のWebアプリでは、認証済み情報をそのまま使うというより、DBに「あるユーザーが認証済みであるかどうか」を記録すると思いますが、今回は簡単のために上記仕様としています。
:::

### 実装
#### 事前準備
- [ ] Vonageにユーザ登録し、Verify APIのAPI KeyとAPI Secretを取得する
- [ ] 取得した情報を`VONAGE_API_KEY`, `VONAGE_API_SECRET`という名前で環境変数に登録する

#### 環境・SW関連
- Go1.19.2
- Webフレームワーク: echo v4.9.1
    - echo middleware: echo-contrib(session) v0.13.0

#### 詳細
コード全体は以下のリポジトリに保存しました。

https://github.com/tenkoh/app-vonage-sample

:::note warn
あくまでサンプルアプリケーション向けのため、入力のバリデーションや、アクセス制限など、プロダクションを想定した時に必要な設定は不足しています。
:::

本節ではポイントを絞って解説していきます。

##### Sessionを使ったWebアプリ内の状態保持
ユーザーが電話番号をWebアプリに送信した後、WebアプリからVonageに認証プロセスの開始をリクエストします。リクエストが成功すると、この後のPINコード確認に必要な`requestID`がVonageからWebアプリに返されるため、これをWebアプリ内で保持しておく必要があります。

Node.jsの例でもサーバサイドでの状態保持にsessionを利用しています。Goでは標準モジュールでセッションをサポートしていないため、自作するか公開モジュールを利用しますが、今回はWebフレームワークのechoの中でsessionを使っていきます。（なお内部的には`github.com/gorilla/sessions`が使われています)

```go
func verify(c echo.Context) error {
	tel := c.FormValue("tel")
	if !isValidTel(tel) {
		return c.HTML(
			http.StatusBadRequest,
			`<html><p>invalid telephone number</p><a href="/">go to home</a></html>`,
		)
	}
	// start verify process using vonage api
	reqID, err := requestVerify(c) //この関数の中身は後ほど紹介します
	if err != nil && !errors.Is(err, errConcurrentRequest) {
		return c.String(http.StatusInternalServerError, err.Error())
	}

	// save requestID into session
	sess, err := session.Get(sessionName, c)
	if err != nil {
		return c.String(http.StatusInternalServerError, fmt.Sprintf("fail to call session: %s", err))
	}
	sess.Options = &sessions.Options{
		Path:     "/",
		MaxAge:   sessionMaxAge,
		HttpOnly: true,
		Secure:   true,
	}
	sess.Values["requestID"] = reqID
	sess.Save(c.Request(), c.Response())

	// show form to send pin code
	return c.Render(http.StatusOK, "pinform", nil)
}
```

- この例では簡単のため、UXを損ねるかもしれませんが、Vonageへの認証リクエスト、応答待ち、PIN入力フォーム表示を順番に実施しています。
    - ただ試してみて興味深かったのですが、Vonageへの認証リクエストから応答が返ってくるまでのラグはほとんど気になりませんでした。高速に応答してくれるのはとても嬉しいですね。
- sessionへのデータ保存はechoのサンプルが大変分かりやすいです。
    - https://echo.labstack.com/middleware/session/
- なお、上記のコードはアプリケーションの状態遷移としては　↓ の部分に相当します。
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1553191/70cf924c-721b-cae7-8a87-74b1026a2d41.png)

sessionはPINコードのチェックでも同様に活用しますが、そちらはリポジトリ上のコードを参照下さい。


##### Vonage SDKを使った認証
電話番号をVonage Verify APIを使って認証するには、認証プロセスごとに割り当てられた`requestID`と、ユーザーにSMSで送信されるPINコード(`pin`)の2つが必要です。

APIを`http.POST`などで直接叩いても良いですが、さまざまなオプションもあるためSDKを使うとより気軽に利用することが可能です。

まず認証プロセスを開始し、RequestIDを取得する処理を説明します。

```go
func requestVerify(c echo.Context) (reqID string, _ error) {
	auth := vonage.CreateAuthFromKeySecret(apiKey, apiSecret)
	verifyClient := vonage.NewVerifyClient(auth)

	resp, respErr, err := verifyClient.Request(c.FormValue("tel"), brandName, vonage.VerifyOpts{
		// https://developer.vonage.com/api/verify
		PinExpiry:  sessionMaxAge,
		Lg:         "ja-jp",
		WorkflowID: 6, //only sms (one time)
	})
	// some error handling...

	return resp.RequestId, nil
}
```

- 処理はとてもシンプルで、事前に取り込んでおいた`apiKey`と`apiSecret`を用いてクライアントを生成し、それに認証する電話番号(`c.FormValue("tel")`)を渡しています。
    - このとき、オプションをいくつか設定しています。
        - `PinExpiry`はPINコード確認を終えるまでの有効期間を秒単位で指定します
        - `Lg`はユーザーに送信するSMSの言語設定に使われます。
        - `WorkflowID`はユーザーに送るメッセージの組み合わせを変えます。デフォルトではSMS+SMS+TELと、3回PINコードが送信されます。
        - Optionの全体は公式ドキュメントから確認することができます。 https://developer.vonage.com/api/verify?theme=dark#verifyRequest
- スペースの都合上エラー処理を割愛して紹介していますが、 **ある電話番号に対する認証が期限切れになる前に、連続して認証開始リクエストを送信してもVonageからエラーを返してくれます。また進行中の認証の`requestID`も返却してくれます。Webアプリ側で配慮することがいろいろと減らせて嬉しいですね。**

:::note info
なお、Optionで`PinExpiry`を渡せるのですが、SDK上で未実装になっており設定が反映されません :laughing: プルリク出したのでお願いしますね！

:::



次にPINコードの確認を行う処理を紹介します。

```go
func requestCheck(c echo.Context) (verified bool, _ error) {
	auth := vonage.CreateAuthFromKeySecret(apiKey, apiSecret)
	verifyClient := vonage.NewVerifyClient(auth)

    // sessionに保持してあるrequestIDを読み出す
	sess, err := session.Get(sessionName, c)
	if err != nil {
		return false, fmt.Errorf("fail to call session: %w", err)
	}
	r := sess.Values["requestID"]
	reqID, ok := r.(string)
	if !ok {
		return false, errors.New("invalid requestID in this session")
	}
	if reqID == "" {
		return false, errors.New("invalid requestID in this session")
	}

    // ユーザがフォームで入力したPINコードを読み出す
	pin := c.FormValue("pin")
	if !isValidPin(pin) {
		return false, errors.New("invalid pin code")
	}
	_, respErr, err := verifyClient.Check(reqID, pin)
    
    // some error handling...
    
    // 何もエラーがなければ認証OKとして返す
	return true, nil
}
```

- こちらもシンプルに、sessionに保存しておいたrequestIDと、ユーザーがフォームで入力したPINコードをそれぞれ取り出して、VonageのVerify APIに渡しています。

**シンプルに電話番号認証が実装できましたね！**

## おわりに
去年よりも一歩踏み込んでVonageさんのサービスに触れることができたように思います。簡単に電話番号認証ができたので、これで私も2FA使いになれますかね…。こういった試みも何かきっかけがないと始めづらいので、去年に続き今年もアドベントカレンダーを企画してくださったVonageさんに感謝です :smile: