Two-factor authentication (2FA) adds an extra layer of security for users that are accessing sensitive information. In this tutorial, we will cover how to implement two-factor authentication for a user's phone number with Nexmo's Verify API endpoints.
After reading the blog post about how to set up a server to use Nexmo Verify you're now ready to set up an Android app to network with the server.
Prequisites
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
This app will have only two dependencies: Retrofit for making network calls and Moshi for serializing and deserializing JSON.
The app will need to do a few things. Store a requestId
so that a verification request can be canceled or completed. As well as make a network call to three endpoints:
Start a verification
Check a verification code
Cancel a verification request
To get started, I've set up a simple demo app with a login screen asking for the user's email address, password, and phone number for two-factor authentication (2FA). Clone the following repo and navigate to the getting started branch:
git clone git@github.com:nexmo-community/verify-android-example.git
cd verify-android-example
git checkout getting-started
Integrating with Proxy Server
In the blog post about how to set up a proxy server for the Verify API, we covered three endpoints:
Make a verification request.
Check a verification code.
Cancel a verification request.
So we're going to need to have our app send POST
s to those three endpoints. To do this we're going to use Retrofit and Moshi.
Making Network Requests
The build.gradle file should include the following dependencies. Retrofit for networking, Moshi for JSON parsing, and OkHttp for logging.
//app/build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
We can start using them to network with our proxy API.
First, we'll set up an interface for the three endpoints our app needs to hit:
public interface VerifyService {
@POST("request")
Call<verifyresponse> request(@Body PhoneNumber phoneNumber);
@POST("check")
Call<checkverifyresponse> check(@Body VerifyRequest verifyRequest);
@POST("cancel")
Call<cancelverifyresponse> cancel(@Body RequestId requestId);
}
</cancelverifyresponse></checkverifyresponse></verifyresponse>
I won't go over each of the models for the responses, but you can view them in more detail here. For the purpose of this tutorial I've matched the structure of the models to the JSON that Verify API expects.
Now that there is an interface of endpoints, we can write a VerifyUtil
that will instantiate Retrofit and make the network requests:
public class VerifyUtil {
private final Retrofit retrofit;
private VerifyService verifyService;
private static VerifyUtil instance = null;
private VerifyUtil() {
//Use OkHttp for logging
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
retrofit = new Retrofit.Builder()
.client(client)
//change this url to your own proxy API url
.baseUrl("https://nexmo-verify.glitch.me")
.addConverterFactory(MoshiConverterFactory.create())
.build();
verifyService = retrofit.create(VerifyService.class);
}
public static VerifyUtil getInstance() {
if (instance == null) {
instance = new VerifyUtil();
}
return instance;
}
public VerifyService getVerifyService() {
return verifyService;
}
public Retrofit getRetrofit() {
return retrofit;
}
}
I've made VerifyUtil
a singleton that can be called from any Activity.
Let's start by making some changes to our LoginActivity. The signInBtn
already has an OnClickListener that calls start2FA()
so we can add to that method.
Login Activity
private void start2FA(final String phone) {
//clear out the previous error (if any) that was shown.
errorTxt.setText(null);
//start the verification request. Wrap `String phone` in a `PhoneNumber` class so it is correctly serialized into JSON
Call<verifyresponse> request = VerifyUtil.getInstance().getVerifyService().request(new PhoneNumber(phone));
request.enqueue(new Callback<verifyresponse>() {
@Override
public void onResponse(Call<verifyresponse> call, Response<verifyresponse> response) {
if (response.isSuccessful()) {
//parse the response
VerifyResponse requestVerifyResponse = response.body();
storeResponse(phone, requestVerifyResponse);
startActivity(new Intent(LoginActivity.this, PhoneNumberConfirmActivity.class));
} else {
//if the HTTP response is 4XX, Retrofit doesn't pass the response to the `response.body();`
//So we need to convert the `response.errorBody()` to the `VerifyResponse`
Converter<responsebody, verifyresponse=""> errorConverter = VerifyUtil.getInstance().getRetrofit().responseBodyConverter(VerifyResponse.class, new Annotation[0]);
try {
VerifyResponse verifyResponse = errorConverter.convert(response.errorBody());
Toast.makeText(LoginActivity.this, "Error Will Robinson!", Toast.LENGTH_LONG).show();
errorTxt.setText(verifyResponse.getErrorText());
} catch (IOException e) {
Log.e(TAG, "onResponse: ", e);
}
}
}
@Override
public void onFailure(Call<verifyresponse> call, Throwable t) {
Toast.makeText(LoginActivity.this, "Error Will Robinson!", Toast.LENGTH_LONG).show();
Log.e(TAG, "onFailure: ", t);
}
});
}
</verifyresponse></responsebody,></verifyresponse></verifyresponse></verifyresponse></verifyresponse>
Whenever someone clicks on the "Sign In" button, the app will send the phone number to the proxy API we've set up in the earlier blog. This app will ignore anything in the email or password screens since this app is a proof of concept.
If there is an error with the request, the proxy API will send a 400
response and we'll handle it in the else
block of the onResponse()
callback. If there's any other error, the proxy server will respond with a 500
and the app can handle the error in the onFailure()
callback.
If the request is successful, the app will store the phone number the user sent and the responseId
the proxy server returned, then the app will start the PhoneNumberConfirmActivity
so that the user can enter their code. It's important to store the phone number and request ID because those fields are needed to cancel or check the status of any verification request. And while a user may remember their phone number to cancel or check the status of a verification, they can't be expected to remember a multi-character, randomly generated request ID string. So when the app makes a verification request, we'll store the requestId
in SharedPreferences
to retrieve later in case the user backgrounds the app or the activity is restarted.
PhoneNumberConfirmActivity
Once a user enters the phone number and the server responds with a 200
, the app will start the PhoneNumberConfirmActivity. I've already started the activity on the getting-started
branch
The basic structure of the activity is already built. All that's left is to use the requestId
and phone number to confirm the PIN code or cancel the verification.
First, we'll start with fetching the phone number and requestId from SharedPreferences
. We'll need the phone number and requestId
to confirm the verification code or cancel the verification process.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_phone_number_confirm);
//Get user's phone number and request ID from SharedPreferences
SharedPreferences sharedPref = getSharedPreferences(TWO_FACTOR_AUTH, Context.MODE_PRIVATE);
String phoneNumber = sharedPref.getString(PHONE_NUMBER, null);
Log.d(TAG, "phone: " + phoneNumber);
requestId = sharedPref.getString(phoneNumber, null);
Log.d(TAG, "request id: " + requestId);
//Set up rest of views on OnClickListeners...
}
Then once the user clicks on the "Confirm" button, the app needs to send the PIN code and the requestId
to the proxy API. We can wire up the "Confirm" button's OnClickListener
to kick off that request.
private void confirmCode(String code) {
//clear out the previous error (if any) that was shown.
verifyTxt.setText(null);
//Confirm the verification code. Wrap `String code` and requestId in a `VerifyRequest` class so it is correctly serialized into JSON
VerifyUtil.getInstance().getVerifyService().check(new VerifyRequest(code, requestId)).enqueue(new Callback<checkverifyresponse>() {
@Override
public void onResponse(Call<checkverifyresponse> call, Response<checkverifyresponse> response) {
if (response.isSuccessful()) {
verifyTxt.setText("Verified!");
Toast.makeText(PhoneNumberConfirmActivity.this, "Verified!", Toast.LENGTH_LONG).show();
} else {
//if the HTTP response is 4XX, Retrofit doesn't pass the response to the `response.body();`
//So we need to convert the `response.errorBody()` to a `CheckVerifyResponse`
Converter<responsebody, checkverifyresponse=""> errorConverter = VerifyUtil.getInstance().getRetrofit().responseBodyConverter(CheckVerifyResponse.class, new Annotation[0]);
try {
CheckVerifyResponse checkVerifyResponse = errorConverter.convert(response.errorBody());
verifyTxt.setText(checkVerifyResponse.getErrorText());
Toast.makeText(PhoneNumberConfirmActivity.this, "Error Will Robinson!", Toast.LENGTH_LONG).show();
} catch (IOException e) {
Log.e(TAG, "onResponse: ", e);
}
}
}
@Override
public void onFailure(Call<checkverifyresponse> call, Throwable t) {
Toast.makeText(PhoneNumberConfirmActivity.this, "Error Will Robinson!", Toast.LENGTH_LONG).show();
Log.e(TAG, "onFailure: ", t);
}
});
}
</checkverifyresponse></responsebody,></checkverifyresponse></checkverifyresponse></checkverifyresponse>
The network request to confirm the PIN code is similar to the request made earlier to start the verification process. If all goes well the server will respond with a 200
OK and we can let the user know they are authenticated. If the server responds with a 400
or 500
then we can check the errorText
of the response and alert the user of the issue either with a toast or by displaying the error in the verifyTxt
TextView
I've created.
There may be times when users want to cancel a verification request. This may be because they entered the wrong phone number, want to log in with another account, or just don't want to verify themselves at this time. The app needs to handle this scenario so we can add a "Cancel" button to our activity and wire it up to send a cancellation network request. I've already added the "Cancel" button and added an OnClickListener
with a callback of cancelRequest()
, now we can just add the networking code to that method.
private void cancelRequest() {
//clear out the previous error (if any) that was shown.
verifyTxt.setText(null);
//Cancel the verification request
VerifyUtil.getInstance().getVerifyService().cancel(new RequestId(requestId)).enqueue(new Callback<cancelverifyresponse>() {
@Override
public void onResponse(Call<cancelverifyresponse> call, Response<cancelverifyresponse> response) {
if (response.isSuccessful()) {
Toast.makeText(PhoneNumberConfirmActivity.this, "Cancelled!", Toast.LENGTH_LONG).show();
finish();
} else {
//if the HTTP response is 4XX, Retrofit doesn't pass the response to the `response.body();`
//So we need to convert the `response.errorBody()` to a `CancelVerifyResponse`
Converter<responsebody, cancelverifyresponse=""> errorConverter = VerifyUtil.getInstance().getRetrofit().responseBodyConverter(CancelVerifyResponse.class, new Annotation[0]);
try {
CancelVerifyResponse cancelVerifyResponse = errorConverter.convert(response.errorBody());
verifyTxt.setText(cancelVerifyResponse.getErrorText());
Toast.makeText(PhoneNumberConfirmActivity.this, "Error Will Robinson!", Toast.LENGTH_LONG).show();
} catch (IOException e) {
Log.e(TAG, "onResponse: ", e);
}
}
}
@Override
public void onFailure(Call<cancelverifyresponse> call, Throwable t) {
Toast.makeText(PhoneNumberConfirmActivity.this, "Error Will Robinson!", Toast.LENGTH_LONG).show();
Log.e(TAG, "onFailure: ", t);
}
});
}
</cancelverifyresponse></responsebody,></cancelverifyresponse></cancelverifyresponse></cancelverifyresponse>
Wrapping It Up
Now we've implemented all of the endpoints necessary to follow a verification flow. If you'd like to see the finished product of this Android app, the source code is on the finished
branch on GitHub.
Next Steps
If you'd like you can implement the rest of the endpoints in the Verify API. Note that this will require you to add more endpoints in the API proxy server. You can also add additional endpoints to cover the Number Insights API. This will also require you to add more endpoints in the API proxy server. There's also an iOS version of this post. Read more from our developer advocate Eric Giannini.
Risks/Disclaimer
You may want to inspect the SSL certificate of the responses from your proxy API to ensure the client app isn't subject to a MITM attack. For more details, visit the Android developer docs.