[Android]SMS認証のユーザー負担を減らすSMS User Consent APIを使ってみた

サービスのセキュリティを高める方法としてSMSを利用した本人認証があります。SMS認証はセキュリティが向上する反面、電話番号と認証コードの入力が必要なためユーザーにとっては煩わしいです。当記事では、ユーザーの負担を減らすための手段としてSMS User Consent APIを紹介します。
2021.02.05

アプリがSMSから認証コードを取得して自動入力するSMS User Consent APIを使ってみた。

はじめに

サービスのセキュリティを高める方法としてSMSを利用した本人認証があります。SMS認証はセキュリティが向上する反面、電話番号と認証コードの入力が必要なためユーザーにとっては煩わしいです。当記事では、ユーザーの負担を減らすための手段としてSMS User Consent APIを紹介します。このAPIを使うことでパーミッションの追加なしでSMS受信時にユーザーがワンタップするだけでアプリはSMSから認証コードを取得することができます。(サーバーサイドとSMS Retriever APIを組み合わせることで完全に自動で認証コードを取得することが出来ますが、今回は扱いません)

確認ダイアログ

認証コードの取得

今回実装したソースコードはこちら↓
- (GitHub)sms-user-consent-api-sample

開発環境

  • OS: macOS Catalina
  • Android Studio: 4.1.2
  • Language: Kotlin 1.4.21

制限事項

SMS User Consent APIには、いくつかの制限があります。以下のケースではSMSを受信してもユーザーに許可を求めるダイアログは表示されません。

  • SMSに数字を1つ以上含んでいる4~10桁の英数字が存在しない
  • ユーザーの連絡先に登録されている差出人からのメッセージ

実装手順

  1. ライブラリを追加する
  2. レイアウトファイルの編集
  3. MainActivityの実装

アプリレベルのbuild.gradleにライブラリを追加

アプリレベルのbuild.gradleに以下に示した2つのライブラリを追加します。

app/build.gradle

dependencies {
    // 省略
    implementation "com.google.android.gms:play-services-auth:19.0.0"
    implementation "com.google.android.gms:play-services-auth-api-phone:17.5.0"
}

レイアウトの編集

activity_main.xmlに認証コードを入力するEditTextを配置します。

src/main/res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editTextTextPersonName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Enter verification code"
        android:inputType="text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivityの編集

SMS受信時のおおまかな流れを説明します。端末がSMSを受信するとシステムがBroadcastReceiverのonReceiveメソッドを呼び出します。startActivityForResultメソッドを使ってアプリ上に確認ダイアログを表示します。ユーザーが確認ダイアログのAllowボタンをタップするとonActivityResultメソッドが呼び出されます。ここでSMSメッセージを解析して認証コードを取得します。今回のサンプルアプリではEditTextに取得した認証コードを表示しましたが、代わりにサーバーサイドへ認証コードを送信することでさらにユーザー操作を減らすことができます。

シーケンス図

src/main/java/com/mos1210/android/example/smsuserconsent/MainActivity.kt

import android.app.Activity
import android.content.*
import android.os.Bundle
import android.util.Log
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status

class MainActivity : AppCompatActivity() {

    companion object {
        val TAG: String = MainActivity::class.java.simpleName
        private const val REQUEST_CODE = 1
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onResume() {
        super.onResume()

        val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
        registerReceiver(smsVerificationReceiver, intentFilter)

        // SMSメッセージの待ち受け開始
        SmsRetriever.getClient(this).startSmsUserConsent(null)
    }

    override fun onPause() {
        super.onPause()

        // SMSメッセージの待ち受け解除
        unregisterReceiver(smsVerificationReceiver)
    }

    private val smsVerificationReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context?, intent: Intent?) {
            if (SmsRetriever.SMS_RETRIEVED_ACTION == intent?.action
                && intent.extras != null
            ) {
                val status = intent.extras?.get(SmsRetriever.EXTRA_STATUS) as Status
                when (status.statusCode) {
                    CommonStatusCodes.SUCCESS -> {
                        val consentIntent =
                            intent.extras?.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
                        try {
                            startActivityForResult(consentIntent, REQUEST_CODE)
                        } catch (e: ActivityNotFoundException) {
                            e.printStackTrace()
                        }
                    }
                    CommonStatusCodes.TIMEOUT -> {
                        Log.d(TAG, "timeout")
                        // 5分でタイムアウト
                    }
                }
            }
        }
    }

    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {

            REQUEST_CODE ->
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE) ?: return
                    val oneTimeCode = parseOneTimeCode(message)

                    findViewById<EditText>(R.id.editTextVerificationCode).setText(oneTimeCode)
                }
        }
    }

    private fun parseOneTimeCode(message: String): String {
        // SMSメッセージに合わせて文字列を処理する
        return message.split("\n")[0].split(":")[1]
    }
}

検証方法

エミューレーターに対してadbコマンドでSMSを送信します。最初にエミュレータを起動してからTerminalでadb devicesコマンドを入力しテスト対象となるデバイスを調べます。

$ adb devices
List of devices attached
emulator-5554   device ←対象のエミュレーター

次に、以下のadbコマンドで対象のEmulatorにSMSを送信します。この例では電話番号が「09012345678」、SMSの内容が「verification code:123456」です。

$ adb -s emulator-5554 emu sms send 09012345678 verification code:123456
OK

実行結果

SMS受信時に確認ダイアログが表示され、ユーザーがAllowボタンをタップすると認証コードが自動入力されました。

実行結果

まとめ

SMS User Consent APIを使ってSMSから認証コードを取得して自動入力することができました。もっと別な方法があるよ!などあればTwitterやコメントで教えていただければ嬉しいです。

参考