Fax Verification by Twilio Verify and Twilio Programmable Fax

この投稿は、[Twilio Advent Calendar 2020](https://qiita.com/advent-calendar/2020/twilio)の20日目の記事になります。
昨日(19日目)の投稿は @oic0310 さんの[Twilioアドベントカレンダー 12月19日 パスワードは電話で](https://note.com/mtk_oishi/n/n0ed8fab78270)でした。

#はじめに
昨今、不正ログインなどのセキュリティ対策として、二要素認証を必須とするサービスが増えてきました。一般的なものとしては、SMSや音声電話、Authyなどのアプリを利用することが多いと思います。ただ、これまで二要素認証で「Fax」を利用したケースをみたことがなかったので、今回、作ってみました。

2021年12月17日（太平洋時間）をもちまして、「Twilio Programmable Fax」の提供を終了するとのこと。追悼の意を込めて。
【重要】Twilio Programmable Fax サービス終了について
https://cloudapi.kddi-web.com/news/latest/twilio-programmable-fax-end-of-life-on-december-17-2021
もしかしたら、2020年12月20日現在、過去１年間でTwilio Faxを利用していない場合、新規にFaxを利用できないかも。

#デモ動画
こちらをご覧くださいませ。
[https://youtu.be/r4k66y_CF2A](https://youtu.be/r4k66y_CF2A)

#利用シーン
特にないかな・・・(苦笑)
使いみちなさそうだけど、セキュリティはあまり考慮してないので、ご利用は自己責任で。


#構成
![スクリーンショット 2020-12-20 22.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110436/b33004c0-4ddd-cbb2-09ca-6e3479d0fb32.png)
ざっくり説明すると、、、

1. 二要素認証はメール認証にする。
1. メール認証の宛先はGmailとし、Fax番号をアドレスのエイリアス(アカウントの末尾に「+」をつけると、そのアカウントでメール受信できる)
1. Google Apps Scriptで受信メールを取得し、アドレスからFax番号を、本文から認証コードを取得。
1. Google Apps ScriptからTwilio Programmable Fax APIをコールし、Faxを送付する

という感じです。

#使ったサービスやツール
- [Twilio Programmable Fax](https://www.twilio.com/docs/fax)
    - APIでFaxを送付することができるサービス
- [Twilio Verify](https://www.twilio.com/docs/verify/api)
    - Twilioが提供している二要素認証の仕組みの一つ。認証コード送信とコード認証機能のAPIが提供される
- [SendGrid](https://sendgrid.kke.co.jp/)
    - みなさんご存知のSendGrid。メール配信サービス。Twilio Verifyでメール認証する場合に必要。
- [Twilio Functions](https://jp.twilio.com/docs/runtime/functions)
    - Twilioが提供するFaaS。Node.jsで書いたコードを実行します。
    - いわゆる、AWS Lamdbaのようなものです。
- [Twilio Assets](https://jp.twilio.com/docs/runtime/assets)
    - Twilioが提供するウェブホスティング可能なストレージサービスです。
    - いわゆるAmazon S3のようなものです。
- [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart)
    - Twilioをコマンドラインより操作できる機能
- [Twilio CLI Serverless Toolkit](https://www.twilio.com/docs/labs/serverless-toolkit)
    - Twilio CLIのプラグイン。TwilioのAssetsやFunctionsを使ったApplicationをローカル環境で開発でき、また、Twilio環境に簡単にデプロイできる機能です。
- [Gmail](https://www.google.com/intl/ja/gmail/about/)
    - みなさんご存知のGmail
- [Google Spreadsheet](https://www.google.com/intl/ja_jp/sheets/about/)
    - みなさんご存知のSpreadsheet
- [Google Apps Script](https://developers.google.com/gsuite/aspects/appsscript?hl=ja)
    - Googleの各種サービスをプログラムで操作できる機能。Excelでいうところのマクロみたいなやつ。

#作ってみる
##ソースコード
- Twilio Serverless側：[https://github.com/takeshifurusato/2fafax](https://github.com/takeshifurusato/2fafax)
- Google Apps Script側：以下、説明に記載

##Twilio Verifyの設定
基本的に、以下のURLに記載されたとおりに行えばOKです。
[https://cloudapi.kddi-web.com/magazine/two-factor-authentication/how-to-implement-twilio-verify](https://cloudapi.kddi-web.com/magazine/two-factor-authentication/how-to-implement-twilio-verify)
とてもわかりやすくまとめてくださってます。KDDIウェブコミュニケーションズ様に感謝。
ちなみに、メール認証をする場合は、Twilioの傘下に入っているSendGridを利用する必要があります。
手順は以下のドキュメントを参照すればOKです。
[https://www.twilio.com/docs/verify/email](https://www.twilio.com/docs/verify/email)

途中、認証メール送付用のテンプレートを作成する必要がありますが、今回は、Google Apps Scriptが処理をするので以下のようにシンプルにしておきます。

```
{{twilio_code}}
```

##Twilio Serverlessの作成
ここでやることは以下の２つです

1. Fax番号を入力させ、それを含んだメールアドレスを生成、Twilio Verifyに渡す。
1. 認証コードを入力させ、上記のメールアドレスと一緒に、Twilio Verifyに渡す。

そして、注意点としては、Twilio Verify APIをどこでコールするかということです。APIをコールするためには、AUTH_TOKENが必要となります。これは外部に漏らしてはいけない情報なので、Javascript側に記述してはダメです。
従って、Twilio Verify APIのコールは、Twilio Serverless環境のFunctionsでこの処理を記載することにします。

つまり、認証コード送信は、
Twilio Assetsで公開したHTMLを利用しFax番号を入力
→JavaScriptでTwilio Functionsを呼び出しFax番号を送付
→Twilio FunctionsでFax番号をメールアドレスに変換、Twilio Verify APIをコール

確認コード認証は、
Twilio Assetsで公開したHTMLを利用し認証コードを入力
→JavaScriptでTwilio Functionsを呼び出しFax番号と認証コードを送付
→Twilio FunctionsでFax番号をメールアドレスに変換、認証コードを使って、Twilio Verify APIをコール

ということです。以下、ソースコードです。
[https://github.com/takeshifurusato/2fafax](https://github.com/takeshifurusato/2fafax)
以下、入力画面のイメージです。
![スクリーンショット 2020-12-20 3.20.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110436/1c23fdc3-f5f3-a36e-3c78-d6379b09b415.png)
ちなみに .envは以下のような内容です。

```
ACCOUNT_SID=Twilio ACCOUNT SID
AUTH_TOKEN=Twilio AUTH TOKEN
VERIFY_SERVICE_SID=Twilio VerifyのID
MAIL_ACCOUNT=Gmailのアカウント
MAIL_DOMAIN=gmail.com
```


##Google Apps ScriptでFax送信
Twilio Programmable Faxは、送信する内容を、Twilioからアクセスが可能な公開されている場所にPDF形式で設置する必要があります。
ここでやることは以下の３つです。

1. Twilio Verifyから送付された認証コードを受信する
1. 認証コードを埋め込んだPDFファイルを生成し、公開されている場所に設置する
1. Twilio Programmable Fax APIをコールし、Fax送付

特に悩んだのは、 「認証コードを埋め込んだPDFファイルを生成し、公開されている場所に設置する」です。これは、Google Apps Scriptで、Gmailを取得、本文から認証コードを取得、その内容をGoogle Spreadsheetに埋め込み、そのGoogle SpreadsheetをPDF保存、そのPDFのダウンロードリンクをTwilio Programmable Faxに送付しました。
また、Google Apps ScriptでGoogle Spreadsheetの内容を変更した場合、そのGoogle Apps Scriptのプロセスが終了するまで、Google Spreadsheetの内容が反映されないという特徴があります。これは「認証コード埋め込み」と「PDF保存」とが1つのプロセスでできないということです。
そして、生成したPDFファイルはGoogleDriveに保存され、それをsetSharingでTwilioからアクセスできるように公開にしています。また、URLの一部を書き換えることで、Viewモード(GoogleDriveで表示する)から、直ダウンロードできるようにしています。そうしないとTwilioからアクセスしたときにPDFとして認識されません。

以下、スクリプトです。

```javascript
const accountSid = Twilio_ACCOUNT_SIDを記入;
const authToken = Twilio_AUTH_TOKENを記入;
const from_number = '+8150******** Fax送付元のTwilio番号';

function start() {
    // PDF作成→送信は２巡目で。
    // GASの仕様でスクリプトが終わるまでは書き換えたスプレッドシートが保存されない(PDF化できない)
    makePdfFax()
    getMail()
}

function makePdfFax() {
    var sheetObj = SpreadsheetApp.openById('管理用一覧スプレッドシートのID');
    var sheet = sheetObj.getSheetByName('history');
    const lastRow = sheetObj.getLastRow();
    for (let i = 1; i <= lastRow; i++) {
        if (sheet.getRange(i, 2).getValue()) {
            continue;
        }

        var files = DriveApp.getRootFolder().getFilesByName(sheet.getRange(i, 1).getValue());
        var file = files.next();
        var pdf_sheetObj = SpreadsheetApp.openById(file.getId());
        var pdf = pdf_sheetObj.getAs('application/pdf');
        var pdfname = sheet.getRange(i, 1).getValue() + ".pdf";
        var shareUrl = DriveApp
            .createFile(pdf)
            .setName(pdfname)
            .setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW)
            .getUrl();

        shareUrl = shareUrl.replace("https://drive.google.com/file/d/", "https://drive.google.com/uc?id=");
        shareUrl = shareUrl.replace(/\/view.*/, "");

        var fax_number = sheet.getRange(i, 3).getValue()
        var fax_number_E164 = "+81" + (/^0([0-9]+)/g).exec(fax_number)[1];
        sendFax(fax_number_E164, shareUrl);

        sheet.getRange(i, 2).setValue(1)
    }
}

function getMail() {
    var sheetObj = SpreadsheetApp.openById('管理用一覧スプレッドシートのID');
    var sheet = sheetObj.getSheetByName('history');

    var thds = GmailApp.search("label:inbox is:unread");

    var row = sheet.getLastRow() + 1;
    var row_first = row;
    var oldIds = new Array();
    for (var i = 1; i <= row; i++) {
        oldIds.push(sheet.getRange(i, 1).getValue());
    }

    for (var n in thds) {
        var thd = thds[n];
        var msgs = thd.getMessages();
        for (m in msgs) {
            var msg = msgs[m];
            var date = msg.getDate();
            var body = msg.getBody();
            var id = msg.getId();
            var to = msg.getTo();
            var from = msg.getFrom();

            if (oldIds.indexOf(id) > -1) {
                continue;
            }

            var number = (/\+([0-9]+)/g).exec(to)
            var auth_code = (/^([0-9]+)/g).exec(body)

            var org = SpreadsheetApp.openById('Fax送信用のテンプレートスプレッドシートのID');
            var ss = org.copy(id);
            ss.getRange("A2").setValue(auth_code[1]);

            sheet.getRange(row, 1).setValue(id);
            sheet.getRange(row, 2).setValue(0);
            sheet.getRange(row, 3).setValue(number[1]);
            sheet.getRange(row, 4).setValue(auth_code[1]);
            row++;

        }
    }
}

function sendFax(to, media) {
    const url = "https://fax.twilio.com/v1/Faxes"
    const data = {
        From: from_number,
        To: to,
        MediaUrl: media
    };
    const options = {
        "method": "post",
        "payload": data,
        "headers": {"Authorization": " Basic " + Utilities.base64Encode(accountSid + ":" + authToken)},
        "muteHttpExceptions": true
    };

    try {
        var response = UrlFetchApp.fetch(url, options);
        var content = response.getContentText("UTF-8");
        Logger.log(content);
    } catch (e) {
        Logger.log(e.message);
    }
}
```
管理用一覧スプレッドシートは、空のシートでよいです。「history」という名前でシートを作っておきます。また、Fax送信用のテンプレートスプレッドシートは以下のようなものを作っておきます。
![スクリーンショット 2020-12-20 3.40.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110436/d5f66033-cb29-a8f5-c1df-224b8690bf15.png)


なお、このスクリプトの起動は、トリガー設定が必要となります。
※最短の1分間隔で、start()を指定しておきます。設定のイメージは以下。
![スクリーンショット 2020-12-20 3.48.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110436/fc3f6fa3-7147-1cff-e57b-138263a7068c.png)

#まとめ
- Twilio Verify 超簡単に実装できる（料金は1回の認証で$0.05）
- Twilio Programmable Fax がサービス終了なのは、ちょっと残念。セツナイ。

#余談
- Fax実機は[Twilio SIP Registration](https://jp.twilio.com/docs/voice/api/sip-interface)と、Grandstream HT802を利用してTwilio番号で受信しています。
- そして、Fax実機が壊れそうです。Twilio Programmable Fax のサービス終了が早いか、Fax実機が壊れるのが早いか・・・。

明日(21日目)は、@asaborake さんの[問い合わせが来たらFAXで受信。。。というネタを書こうと思ったらTwilio Fax終了のお知らせ](https://qiita.com/asaborake/items/119788ea836283912e0e)です。