Pada setiap proses otentikasi biasanya diperlukan verifikasi pengguna, umumnya verifikasi dilakukan via email ataupun SMS OTP (One Time Password). Pada aplikasi android verifikasi via SMS lebih disukai karena prosesnya terbilang lebih mudah dan lebih cepat. Disini saya akan coba sharing cara mengimplementasikan SMS OTP menggunakan SMS Retriever API dari Google.

Artikel ini hanya akan lebih fokus membahas implementasi dari sisi Android. Untuk implementasi dari sisi server silahkan baca dokumentasi resmi.

SMS Retriever API

SMS Retriever API merupakan bagian dari Google Play Services. API ini memungkinkan kita untuk mendeteksi SMS yang masuk secara otomatis untuk kemudian kita gunakan pada proses verifikasi pengguna. Keuntungan menggunakan API ini salah satunya adalah adalah kita tidak perlu menggunakan android.permission.RECEIVE_SMS lagi untuk dapat mengakses SMS yang masuk, selain itu metode ini ini juga sudah tidak direkomendasikan karena alasan untuk melindungi privasi pengguna.

Implementasi Sisi Android

Tambahkan komponen Play Services Auth ke build.gradle di dalam folder app

implementation 'com.google.android.gms:play-services-auth:19.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'

Buat BroadcastReceiver untuk handle konten sms yang akan masuk

class SMSReceiver : BroadcastReceiver() {

    interface SMSListener{
        fun onOTPReceived(otp: String)
    }

    private var listener: SMSListener? = null

    override fun onReceive(context: Context?, intent: Intent?) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION == intent?.action) {
            val extras = intent.extras
            val status = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
            when (status.statusCode) {
                CommonStatusCodes.SUCCESS -> {
                    val message = extras.get(SmsRetriever.EXTRA_SMS_MESSAGE) as String
                    val otpPattern = Pattern.compile("(\\d{5})") //contoh regex yang mengambil 5 digit kode OTP
                    val matcher = otpPattern.matcher(message)
                    if(matcher.find()){
                        listener?.onOTPReceived(matcher.group(0)!!)
                    }else{
                        Log.w("SMSReceiver", "regex failed to get otp code")
                    }
                }
            }
        }
    }

    fun setListener(listener: SMSListener) {
        this.listener = listener
    }

}

Konfigurasikan kelas SMSReceiver dan jalankan SmsRetriever pada Activity tempat kita untuk menginput kode OTP

class OTPActivity: AppCompatActivity(), SMSReceiver.SMSListener {

    private lateinit var smsReceiver: SMSReceiver

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

    override fun onDestroy() {
        unregisterReceiver(smsReceiver)
        super.onDestroy()
    }

    private fun startSMSRetriever(){
        smsReceiver = SMSReceiver()
        smsReceiver.setListener(this)
        registerReceiver(smsReceiver, IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION))

        val smsRetrieverClient = SmsRetriever.getClient(this)
        val smsRetrieverTask = smsRetrieverClient.startSmsRetriever()
        smsRetrieverTask.addOnCompleteListener { task->
            if(task.isSuccessful){
                Log.i("OTPActivity", "smsRetrieverClient task success")
            }else{
                Log.e("OTPActivity", task.exception?.message!!)
            }
        }
    }

    override fun onOTPReceived(otp: String) {
        //handle your otp here
    }

}

Kode diatas melakukan setup pada method startSMSRetriever untuk kemudian mengambil kode OTP pada method onOTPReceived. Jangan lupa juga untuk melakukan unregister SMSReceiver di method onDestroy.

Pengujian

Sebelum melakukan pengujian pada kode yang sudah kita buat, kita perlu melakukan setup 11-character hash untuk mengidentifikasi sms yang masuk. Penjelasan lengkap tentang ini dapat dilihat di dokumentasi ini, tapi jika menurut kalian menggunakan command-line cukup rumit, disini kita bisa coba cara lain yaitu menggunakan class helper.

buat kelas AppSignatureHelper, kelas ini bertujuan untuk melakukan generate 11-caracter hash berdasarkan package-name dan certificate yg kita gunakan.

class AppSignatureHelper(context: Context?) : ContextWrapper(context) {

    private lateinit var signatures: SigningInfo

    val appSignatures: ArrayList<String>
        @RequiresApi(Build.VERSION_CODES.P)
        get() {
            val appCodes = ArrayList<String>()
            try {
                val packageName = packageName
                val packageManager = packageManager

                signatures = packageManager.getPackageInfo(
                    packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
                ).signingInfo

                for (signature in signatures.apkContentsSigners) {
                    val hash = hash(packageName, signature.toCharsString())
                    if (hash != null) {
                        appCodes.add(String.format("%s", hash))
                    }
                }
            } catch (e: PackageManager.NameNotFoundException) {
                Log.e(TAG, e.message!!)
            }

            return appCodes
        }

    companion object {
        val TAG = AppSignatureHelper::class.java.simpleName

        private val HASH_TYPE = "SHA-256"
        val NUM_HASHED_BYTES = 9
        val NUM_BASE64_CHAR = 11

        private fun hash(packageName: String, signature: String): String? {
            val appInfo = "$packageName $signature"
            try {
                val messageDigest = MessageDigest.getInstance(HASH_TYPE)
                messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
                var hashSignature = messageDigest.digest()

                // truncated into NUM_HASHED_BYTES
                hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES)
                // encode into Base64
                var base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING or Base64.NO_WRAP)
                base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR)

                Log.d(TAG, String.format("pkg: %s -- hash: %s", packageName, base64Hash))
                return base64Hash
            } catch (e: NoSuchAlgorithmException) {
                Log.e(TAG, e.message!!)
            }

            return null
        }
    }
}

Jalankan kelas ini pada Activity Launcher

//disable this in production code
val appSignatureHelper = AppSignatureHelper(applicationContext)
appSignatureHelper.appSignatures

Jalankan aplikasi, kita akan menemukan 11-caracter hash di logcat, kira-kira formatnya seperti ini

D/AppSignatureHelper: pkg: id.adiandrea.otpexample -- hash: aA2MtPvtEex

Kode aA2MtPvtEex inilah yang akan kita gunakan sebagai identifier di konten sms nanti. Selanjutnya kita bisa melakukan simulasi menerima sms masuk dengan emulator bawaan android studio. Konten sms yang kita gunakan pada contoh ini adalah OTP dengan 5 digit.

<#> your otp: 32451

aA2MtPvtEex

Jalankan aplikasi, lalu kirim sms simulasi pada activity untuk input OTP, kode OTP akan langsung terinput.

Contoh kode sumber yang lengkap dapat kalian cek pada repositori ini.

Referensi:

  • https://developers.google.com/identity/sms-retriever/
  • https://nhkarthick.medium.com/android-verify-otp-automatically-with-sms-retriever-api-177168b623ae