0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ngrok + Spark + TwilioでIVR認証を実装してみた

Last updated at Posted at 2021-02-02

前回のTOTP認証に続き、ついでにIVR認証をこの機会に実装してみた。

IVR認証とは、本人の電話番号に直接架電することで、認証ユーザーとしての正当性を担保する認証機能である。
今回は、二段階認証で用いられる一時的な認証コードを自動音声により通知し、その場で電話番号による認証コード入力による認証機能を実装した。

今回使用したTwilioは、自動音声機能はもちろん、SMS通知、チャット、等様々なコミュニケーション用APIを兼ね備えているサービスである。
また、WebhookURLで指定するURLには、当然Twilioからアクセス可能なサーバの宛先が必要なため、ngrokを使用することでAPIを外部公開させた。

※ ngrokについては、以下を参照。
https://qiita.com/hirokisoccer/items/7033c1bb9c85bf6789bd
簡単にいうと、ローカルサーバーを外部公開することが可能なツールである。

①Twilioアカウント発行
https://www.twilio.com/ja/

②アカウント発行すると、「ACCOUNT SID」/「AUTH TOKEN」が自動で発行される。追加でTwilioから電話番号を購入(※今回は無料トライアル枠を使用)
キャプチャ1.JPG

③WebhookURLに電話応答後のリダイレクト先URLを指定
キャプチャ.JPG

④自動音声通知/認証コード認証API


package com.example.demo.ivr;

import static spark.Spark.get;
import static spark.Spark.post;

import java.net.URI;
import java.security.SecureRandom;

import com.twilio.http.HttpMethod;
import com.twilio.http.TwilioRestClient;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.rest.api.v2010.account.CallCreator;
import com.twilio.twiml.VoiceResponse;
import com.twilio.twiml.voice.Gather;
import com.twilio.twiml.voice.Say;
import com.twilio.twiml.voice.Say.Language;
import com.twilio.type.PhoneNumber;

public class TwilioOtpAuth {
    
    private static final String ACCOUNT_SID = "AC8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    
    private static final String AUTH_TOKEN = "c24xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    
    private static final String TWILIO_NUMBER = "+19xxxxxxxx";
    
    private static final String NGROK_BASE_URL = "http://xxxxxxxxx.ngrok.io";
    
    private static final int OTP_LENGTH = 6;
    
    private static TwilioRestClient client = new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN).build();
    
    public static void main(String[] args) {
        
        /**
         * ワンタイムパスコード認証
         */
        post("/otp/check", (req, res) -> {
            
            String digits = req.queryParams("Digits");
            
            IvrOtpDAO ivrOtpDAO = new IvrOtpDAO();
            String otp = ivrOtpDAO.getOtp("1a2b3c");
            
            if (otp != null) {
                if (digits.equals(otp)) {
                    Say success = new Say.Builder("認証に成功しました。").language(Language.JA_JP).build();
                    VoiceResponse successRes = new VoiceResponse.Builder().say(success).build();
                    
                    return successRes.toXml();
                }
            }
            
            Say fail = new Say.Builder("認証に失敗しました。").language(Language.JA_JP).build();
            VoiceResponse failRes = new VoiceResponse.Builder().say(fail).build();
            
            return failRes.toXml();
            
        });
        
        /**
         * ワンタイムパスコード連絡
         */
        post("/otp/announce", (request, response) -> {
            
            byte[] bytes = new byte[OTP_LENGTH];
            SecureRandom secRandom = SecureRandom.getInstance("SHA1PRNG");
            secRandom.nextBytes(bytes);
            
            StringBuilder otp = new StringBuilder();
            for (int i = 0; i < bytes.length; i++) {
                otp.append(secRandom.nextInt(10));
            }
            
            IvrOtpDAO ivrOtpDAO = new IvrOtpDAO();
            ivrOtpDAO.setOtp("1a2b3c", otp.toString());
            
            Say otpMsg = new Say.Builder(
                    "認証コードは" + otp.toString().substring(0, 1) + "、" + otp.toString().substring(1, 2) + "、"
                            + otp.toString().substring(2, 3) + "、" + otp.toString().substring(3, 4) + "、"
                            + otp.toString().substring(4, 5) + "、" + otp.toString().substring(5, 6))
                                    .language(Language.JA_JP).build();
            
            VoiceResponse voiceResponse = new VoiceResponse.Builder().say(otpMsg).build();
            
            Gather gather = new Gather.Builder().action("/otp/check").method(HttpMethod.POST).say(otpMsg).timeout(20)
                    .build();
            
            Say timeoutSay = new Say.Builder("入力が確認できなかったため、通話を終了します。").language(Language.JA_JP).build();
            VoiceResponse timeoutRes = new VoiceResponse.Builder().gather(gather).say(timeoutSay).build();
            
            return timeoutRes.toXml();
        });
        
        /**
         * 架電処理
         */
        get("/call/:number", (request, response) -> {
            
            String phoneNumber = request.params(":number");
            PhoneNumber to = new PhoneNumber(phoneNumber);
            PhoneNumber from = new PhoneNumber(TWILIO_NUMBER);
            URI uri = URI.create(NGROK_BASE_URL + "/otp/announce");
            
            Call call = new CallCreator(to, from, uri).create(client);
            return "calling authentication code...";
        });
    }
    
}

➄認証コード取得/保管DAO


package com.example.demo;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.exceptions.JedisConnectionException;

public class IvrOtpDAO {
    
    private JedisPool pool;
    
    private Jedis jedis = getResource();
    
    /** 認証コード有効期限 */
    private static int RETENTION = 300;
    
    public IvrOtpDAO() {
    }
    
    private Jedis getResource() throws JedisConnectionException {
        synchronized (this) {
            if (pool == null || pool.isClosed()) {
                pool = new JedisPool(new JedisPoolConfig(), "localhost");
            }
        }
        
        return pool.getResource();
    }
    
    /**
     * 認証コード設定
     * 
     * @param deviceId 端末ID
     * @param otp 認証コード
     */
    public void setOtp(String deviceId, String otp) {
        
        String result = jedis.setex(deviceId, RETENTION, otp);
        System.err.println(result);
        
    }
    
    /**
     * 認証コード取得
     * 
     * @param deviceId 端末ID
     * @return 認証コード
     */
    public String getOtp(String deviceId) {
        
        String otp = jedis.get(deviceId);
        
        return otp;
        
    }
}

⑥検証

6-1. GET /call/<電話番号>へリクエスト発行

6-2. 以下のように電話が掛かってくる。電話に出ると、「認証コードは、841086」とアナウンスが流れる。
IMG_0056.png

6-3.電話番号に「841086」を入力する。
IMG_0059.png

6-4. 自動音声で**「認証に成功しました。」**とアナウンスが流れる。

6-5. nagrokのコンソール見ると、①の架電リクエスト(/call/<電話番号>)、②でのWebhookリクエスト(/otp/announce)、③での認証リクエストが正常に実行されていることが確認できる。
test.jpg

6-6. Twilioコンソールから認証リクエスト(/otp/check)へのリクエスト内容を見ると、Degitsパラメータとして、入力した認証コードがPOSTされていることが確認できる。
キャプチャ.JPG

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?