tru.ID logo
Login
Signup

Passwordless Mobile Authentication with Android

In this tutorial, you'll learn how to add passwordless authentication to an Android application signup onboarding flow, using PhoneCheck to verify the phone number associated with the SIM card on a mobile device.

Last updated: April 26, 2021


Before we begin, let's discuss why you would want to use tru.ID PhoneCheck in your onboarding workflows.

20-30% of signup attempts fail on mobile due to incomplete authentication; an SMS is delayed or not delivered at all, an email ends up in spam or is undelivered, a voice call fails, or the user makes a mistake as they navigate the poor UX of a signup flow which uses current legacy authentication options. tru.ID PhoneCheck offers a new way to verify a user, reducing UX friction and improving onboarding conversion rates.

It works by confirming the ownership of a mobile phone number by verifying the possession of an active SIM card with the same number. A mobile data session is created to a unique Check URL for the purpose of this verification. tru.ID then resolves a match between the phone number being verified and the phone number that the mobile network operator identifies as the owner of the mobile data session.

If you just want to dive into the finished code for this tutorial, you can find it on Github.

Before you begin

Before you begin you'll need the following:

Getting Started

Clone the starter-files branch via:

$ git clone -b starter-files --single-branch https://github.com/tru-ID/passwordless-auth-android.git

Create a tru.ID account and you'll end up on the Console containing CLI setup instructions.

Install the tru.ID CLI via:

$ npm i -g @tru_id/cli

Input your tru.ID credentials, which can be found within the tru.ID console.

Install the tru.ID CLI development plugin server.

Create a new tru.ID project within the root directory via:

$ cd passwordless-auth-android && tru projects:create --project-dir .

Run the development server, pointing it to the directory containing the newly created project configuration. This will also open up a localtunnel to your development server, making it publicly accessible to the Internet so that your mobile phone can access it when only connected to mobile data.

$ tru server -t --project-dir .

Starting the project

Open the project up in your Android capable IDE, connect your phone to your computer so it can be used for running the Android project, and run the application from your IDE.

The project should look like this:

First onboarding screen UI

The project's onboarding workflow includes four screens: splash, get started, sign up and signed up.

Click the Get Started button to navigate to the signupFragment UI. It looks like this:

Signup fragment UI

Creating a PhoneCheck

The first step is to create the PhoneCheck. In order to do this, we need to send a POST request with the user's phone number to /phone-check.

We then get back a check_url and check_id which we will use for subsequent calls.

First, add the following dependencies for data fetching in app/build.gradle:

dependencies {
// retrofit and converter
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

Next, create a folder (package) in src/main/java/com/example/tru_phonecheck called api and within that create a subfolder (package) called data.

Within data, create a class called PhoneCheck.kt and add the following code:

package com.example.tru_phonecheck.api.data
import com.google.gson.annotations.SerializedName
data class PhoneCheck( @SerializedName("check_url")
val check_url: String,
@SerializedName("check_id")
val check_id: String) {
}
data class PhoneCheckPost (
@SerializedName("phone_number")
val phone_number: String
)

Here we have two data model classes: PhoneCheck, whose constructor accepts the values of the response, and PhoneCheckPost, whose constructor accepts the user's phone number.

We also use @SerializedName to ensure the values match the expected input / response.

Next, navigate back to the api folder and create another folder (package) named retrofit. Within it, create an interface named RetrofitService and replace its contents with the following:

package com.example.tru_phonecheck.api.retrofit
import com.example.tru_phonecheck.api.data.PhoneCheck
import com.example.tru_phonecheck.api.data.PhoneCheckPost
import retrofit2.Response
import retrofit2.http.*
interface RetrofitService {
@Headers("Content-Type: application/json")
@POST("/phone-check")
suspend fun createPhoneCheck(@Body user: PhoneCheckPost): Response<PhoneCheck>
companion object {
// set up base_url in the format https://{subdomain}.loca.lt gotten from localTunnel URL
const val base_url = "https://{subdomain}.loca.lt"
}
}

Here we make use of the model we created to make the POST request. We also create a companion object, which allows us to access base_url as an object (i.e. RetrofitService.base_url).

Remember to swap out the placeholder URL in base_url with the localTunnel URL.

We now need to grab the user's phone number when the user touches the button and initiate the POST request.

First, head over to src/main/java/com/example/tru_phonecheck/fragments/onboarding/screens/signupFragment.kt and add a function that disables the UI when the button is touched:

private fun setUIStatus (button: Button?, input: EditText, enabled: Boolean){
activity?.runOnUiThread {
button?.isClickable = enabled
button?.isEnabled = enabled
input.isEnabled = enabled
}
}

Next, add the following to create the Retrofit service:

//retrofit setup
private fun rf(): RetrofitService {
return Retrofit.Builder().baseUrl(RetrofitService.base_url).addConverterFactory(GsonConverterFactory.create()).build().create(RetrofitService::class.java)
}

Finally, add the following before return view in the onCreateView function:

...
view.submitHandler.setOnClickListener {
// get phone number
val phoneNumber = phoneNumberInput.text.toString()
Log.d("phone number is", phoneNumber)
// close virtual keyboard
phoneNumberInput.onEditorAction(EditorInfo.IME_ACTION_DONE)
// if it's a valid phone number begin createPhoneCheck
if(!isPhoneNumberFormatValid(phoneNumber)) {
Snackbar.make(container as View, "Invalid Phone Number", Snackbar.LENGTH_LONG).show()
} else {
println("valid number")
// disable the UI
setUIStatus(submitHandler, phoneNumberInput, false)
CoroutineScope(Dispatchers.IO).launch {
try {
val response = rf().createPhoneCheck(PhoneCheckPost(phoneNumber))
if(response.isSuccessful && response.body() != null){
val phoneCheck = response.body() as PhoneCheck
// open checkURL
// get PhoneCheck result
}
} catch(e: Throwable){
Snackbar.make(container as View, e.message!!, Snackbar.LENGTH_SHORT).show()
}
// enable the UI
setUIStatus(submitHandler, phoneNumberInput, true)
}
}
}

Here we attach a click listener to the button with the ID submitHandler. When the button is clicked, we grab the phone number and check if it is valid via a utility function located in src/main/java/com/example/tru_phonecheck/utils/PhoneNumberUtil.kt. If it is a valid number, we disable the UI via setUIStatus and make a network request to create the PhoneCheck using therf().createPhoneCheck function.

invalidated view

Opening the Check URL

The next stage is to open the Check URL returned to us. To do this, we need to use the tru.ID Android SDK. The SDK forces the Check URL network request to go over the mobile data connection so that the mobile network operator and tru.ID can verify the phone number.

Head back to build.gradle and add the following dependency:

dependencies {
...
// tru.ID Android package
implementation 'id.tru.sdk:tru-sdk-android:0.0.3'
}

Next, we need to initialize the SDK on startup. To do that, head over to MainActivity.kt and update it to the following:

package com.example.tru_phonecheck
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import id.tru.sdk.TruSDK
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// initialize Tru SDK
TruSDK.initializeSdk(applicationContext)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
}
}

Head back to SignupFragment.kt and add the following above onCreateView:

private val truSDK = TruSDK.getInstance()

Use the SDK to open the Check URL in the setOnClickListener function:

if(response.isSuccessful && response.body() != null){
val phoneCheck = response.body() as PhoneCheck
// open checkURL
truSDK.openCheckUrl(phoneCheck.check_url)
// get PhoneCheck result
}

We have now successfully opened the Check URL.

Getting the PhoneCheck Response

The last thing we need to do is get the PhoneCheck response.

Head over to PhoneCheck.kt and update its contents:

package com.example.tru_phonecheck.api.data
import com.google.gson.annotations.SerializedName
data class PhoneCheck( @SerializedName("check_url")
val check_url: String,
@SerializedName("check_id")
val check_id: String) {
}
data class PhoneCheckPost (
@SerializedName("phone_number")
val phone_number: String
)
data class PhoneCheckResponse(
@SerializedName("check_id")
val check_id: String,
@SerializedName("match")
val match: Boolean
)

We added a new model, PhoneCheckResponse, which contains two fields: check_id and match. The latter is used to verify the phone number.

Next, head over to RetrofitService.kt and update the code support getting the PhoneCheck resource:

@GET("/phone-check")
suspend fun getPhoneCheck(@Query("check_id") checkId: String): Response<PhoneCheckResponse>

Here we added a GET function to /phone-check?check_id={check_id} where we pass the check_id value dynamically when it is called.

We also map the model type PhoneCheckResponse to the Response object we're expecting.

Let's now head over to SignupFragment.kt and add the functionality to get the PhoneCheck result:

if(response.isSuccessful && response.body() != null){
val phoneCheck = response.body() as PhoneCheck
// open checkURL
truSDK.openCheckUrl(phoneCheck.check_url)
// get PhoneCheck result
val response = rf().getPhoneCheck(phoneCheck.check_id)
if(response.isSuccessful && response.body() != null){
val phoneCheckResponse = response.body() as PhoneCheckResponse
// update UI with phoneCheckResponse
if(phoneCheckResponse.match){
findNavController().navigate(R.id.action_signupFragment_to_signedUpFragment)
} else {
Snackbar.make(container as View, "Registration Failed", Snackbar.LENGTH_LONG).show()
}
}
else {
Snackbar.make(container as View, "An unexpected problem occurred", Snackbar.LENGTH_LONG).show()
}
}

Finally, we get the result of the PhoneCheck using the phoneCheck.check_id we previously got with the PhoneCheck response. If the phone number has been verified, we navigate the user to the signedUpFragment view. If the verification failed (phoneCheckResponse.match is false) we render a "failure" toast.

The signedUpFragment UI looks as follows:

No match view

That's it!

Wrapping Up

With everything in place, we now have a seamless signup onboarding flow with minimal UX friction, resulting in no user drop-offs.

There you have it: you’ve successfully integrated tru.ID PhoneCheck into your Android onboarding workflow.

tru.ID logo

Platform

Docs

DON'T MISS A BEAT — STAY ON THE DOT!

Keep current with industry news and updates from tru.ID.

Follow us on:

Made with ❤️ across the 🌍

© 2021 4Auth Limited. All rights reserved. tru.ID is the trading name of 4Auth Limited.