Installation

The Android SDK is a set of lightweight libraries (267 KB total compressed size for the minimum required components without external dependencies). The libraries are delivered as Android Archive Repository (.aar) files.

Prove manages a maven repository with Android binaries to enable simple integration with Gradle.

Update the dependencies object in the build.gradle file:

dependencies {
  // Existing dependencies are here.
  
  // Add the Prove Link dependencies:
  implementation 'com.prove.sdk:proveauth:6.1.2@aar'
  implementation 'com.prove.sdk:mobileauth:2.9.2@aar'
  implementation 'com.prove.sdk:deviceauth:3.1.1@aar'
  implementation 'com.prove.sdk:base:3.2.0@aar'
}

You'll also need to point to the new repository by updating your settings.gradle file with the Maven repo:

dependencyResolutionManagement {
    // Existing repository settings are here.
  
    repositories {
        // Existing repositories are here.
      
        // Add the Prove Link Maven repository:
        maven {
            url = "https://prove.jfrog.io/artifactory/libs-public-maven/"
        }
    }
}

The following needs to be added to the build.gradle file to also download dependency libraries:

dependencies {
  implementation fileTree('libs')
}

Permissions

Since permissions are automatically merged from the library’s manifest (SDKs) into the main application, the Prove Auth SDK (and its children SDKs) merges the following non-dangerous permissions:

<!-- Required to perform authentication -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- Required to access information about networks -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Required for ConnectivityManager.requestNetwork -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />

Send the Type of Flow: Mobile

Unlike the Web SDK, when using the Android SDK, the mobile flow must be used. You should pass mobile to the Start() function on the server. In a mobile flow, Mobile Auth℠ is attempted first and if that fails, it will perform OTP validation on the mobile phone.

In the mobile flow, once either Mobile Auth or the OTP validation is complete, the AuthFinishStep function will be called.

Authenticate()

The SDK requires an authToken as a parameter for the Authenticate() function. This token is returned from the Start() call of the server SDK. The token is session specific so it can only be used for a single flow so it cannot be reused. It also expires after 15 minutes.

Retrieve authToken

To start the flow, you'll need to send a request to your backend server with the phone number, flow type, and an optional challenge of either the date of birth (format: YYYY-MM-DD) or social security number (format: last 4 digits).

String initialize(String phoneNumber, String ssn, String flowType) {
    YourBackendClient backend = new YourBackendClient(); // Backend API client
  
    // TODO: Build your InitializeRequest object
    InitializeRequest initializeRequest = new InitializeRequest(phoneNumber, ssn, flowType);
  
    // Send an initialize request to your backend server to get authToken
    InitializeResponse response = backend.initialize(initializeRequest);
  
    // TODO: define your own InitializeResponse object to parse authToken string
    return response.getAuthToken();
}

Setup Authenticator

Once you have the authToken, build the authenticator for the mobile flow.

// Object implementing AuthFinishStep interface
AuthFinishStep authFinishStep = new AuthFinishStep() {
   ...
};

// Objects implementing OtpStartStep/OtpFinishStep interfaces
OtpStartStep otpStartStep = new OtpStartStep() {
  ...
};

OtpFinishStep otpFinishStep = new OtpFinishStep() {
  ...
};

ProveAuth proveAuth = ProveAuth.builder()
    .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section
    .withOtpFallback(otpStartStep, otpFinishStep)
    .withContext(this)
    .build();

Cellular data connection might not always be available during testing. The Builder class offers a withTestMode(boolean testMode) method, which permits simulated successful session results while connected to Wi-Fi network only (without a cellular data connection available). This feature is particularly useful for testing on Emulator. This can only be used in Sandbox mode.

ProveAuth proveAuth = ProveAuth.builder()
    .withAuthFinishStep(authId -> verify(authId))
    .withOtpFallback(otpStartStep, otpFinishStep)
    .withContext(this)
    .withTestMode(true) // Test mode flag
    .build();

Performing the Authentication

The ProveAuth object is thread-safe and can be used as a singleton. Most Prove Auth methods are blocking and therefore cannot be performed in the main application thread. The application is required to manage threads, for instance, with an executor service, which provides at least two threads since SDK may have more then one simultaneous blocking requests.

public class MyAuthenticator {
    private final MyBackendClient backend = new MyBackendClient(); // Backend API client

    private final AuthFinishStep authFinishStep = new AuthFinishStep() {
        @Override
        void execute(String authId) {
            try {
                AuthFinishResponse response = backend.authFinish("My App", authId);
                ... // Check the authentication status returned in the response
            } catch (IOException e) {
                String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results";
                // Authentication failed due to request failure
            }
        }
    };

    private ProveAuth proveAuth;

    public MyAuthenticator(Context context) {
        proveAuth = ProveAuth.builder()
          .withAuthFinishStep(authFinishStep)
			    .withOtpFallback(otpStartStep, otpFinishStep)
          .withContext(context)
          .build();
    }

    public void authenticate() throws IOException, ProveAuthException {
        AuthStartResponse response = backend.authStart("My Prove Auth App");

        proveAuth.authenticate(response.getAuthToken());
    }
}

Validate the Mobile Phone

In the AuthFinishStep, you'll specify a function to call once the possession checks are complete on the mobile phone. This endpoint on your backend server will then call the Validate() function to check if the phone number was validated. If it was successful, the server should return the results from the Challenge() function that will include user information. If it was not successful, the server won't return user information. We've included a few example fields that should be returned and then pre-filled on a form for the user to verify.

// Send a verify request to get return user information.
void verify(String: authId) {
  YourBackendClient backend = new YourBackendClient(); // Backend API client
  
  // Build your VerifyRequest object
  VerifyRequest verifyRequest = new VerifyRequest(authId, ...);
                                                  
  // Send a verify request to your backend server to get return user information.
  VerifyResponse response = backend.verify(verifyRequest);
  
  // TODO: define your VerifyResponse object to parse user information from the response
  String firstName = response.getFirstName();
  String lastName = response.getLastName();
  
  // Pre-fill user information to your Android App UI, for example:
  firstNameEditText.setText(firstName);
  lastNameEditText.setText(lastName);
}

OTP Configuration

There are two functions to implement for the OTP handling - a start and a finish step.

In order to set the OTP handlers, OtpStartStep and OtpFinishStep interfaces should be implemented. The Java snippet has an example.

OtpStartStep example:

package com.prove.android_sample_app;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResultRegistry;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

import com.prove.sdk.proveauth.OtpStartInput;
import com.prove.sdk.proveauth.OtpStartStepCallback;
import com.prove.sdk.proveauth.PhoneNumberValidationException;
import com.prove.sdk.proveauth.ProveAuthException;

import java.util.concurrent.atomic.AtomicReference;

public class OtpStartStep implements
        com.prove.sdk.proveauth.OtpStartStep,
        DefaultLifecycleObserver {
    public static final String OTP_START_STEP_KEY = "OtpStartStep";
    private final ActivityResultRegistry registry;
    private ActivityResultLauncher<Intent> launcher;
    private final Context context;
    private final AtomicReference<OtpStartStepCallback> otpStartStepCallbackRef =
            new AtomicReference<>();

    public OtpStartStep(Context context, @NonNull ActivityResultRegistry registry) {
        this.context = context;
        this.registry = registry;
    }

    @Override
    public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException exception,
            OtpStartStepCallback otpStartStepCallback) {
        if (exception instanceof PhoneNumberValidationException) {
            otpStartStepCallbackRef.set(otpStartStepCallback);
            Intent intent = new Intent(context, OtpPhoneNumberInputActivity.class);
            intent.putExtra(IntentExtraFields.OTP_ERROR,
                    "Phone number is invalid, please try again.");
            launcher.launch(intent);
            return;
        }

        if (phoneNumberNeeded) {
            // We need to store callback object, it will be needed to complete this activity
            // by calling it with the results.
            otpStartStepCallbackRef.set(otpStartStepCallback);
            Intent intent = new Intent(context, OtpPhoneNumberInputActivity.class);
            launcher.launch(intent);
        } else {
            otpStartStepCallback.onSuccess(new OtpStartInput(""));
        }
    }

    public void onCreate(@NonNull LifecycleOwner owner) {
        launcher = registry.register(OTP_START_STEP_KEY, owner,
                new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    OtpStartStepCallback callback = otpStartStepCallbackRef.get();
                    if (callback == null) {
                        return;
                    }

                    if (result.getResultCode() == Activity.RESULT_OK) {
                        Intent intent = result.getData();
                        if (intent != null) {
                            String phoneNumber = intent.getStringExtra(
                                    IntentExtraFields.OTP_PHONE_NUMBER_FIELD);
                            callback.onSuccess(new OtpStartInput(phoneNumber));
                        } else {
                            callback.onError();
                        }
                    } else {
                        callback.onError();
                    }
                });
    }
}

OtpFinishStep example:

package com.prove.android_sample_app;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResultRegistry;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

import com.prove.sdk.proveauth.OtpFinishInput;
import com.prove.sdk.proveauth.OtpFinishStepCallback;
import com.prove.sdk.proveauth.OtpValidationException;
import com.prove.sdk.proveauth.ProveAuthException;

import java.util.concurrent.atomic.AtomicReference;

public class OtpFinishStep implements
        com.prove.sdk.proveauth.OtpFinishStep,
        DefaultLifecycleObserver {
    public static final String OTP_FINISH_STEP_KEY = "OtpFinishStep";
    private final ActivityResultRegistry registry;
    private ActivityResultLauncher<Intent> launcher;
    private final Context context;
    private final AtomicReference<OtpFinishStepCallback> otpFinishStepCallbackRef =
            new AtomicReference<>();

    public OtpFinishStep(Context context, @NonNull ActivityResultRegistry registry) {
        this.context = context;
        this.registry = registry;
    }

    @Override
    public void execute(@Nullable ProveAuthException otpException,
            OtpFinishStepCallback otpFinishStepCallback) {
        // We need to store callback object, it will be needed to complete this activity
        // by calling it with the results.
        otpFinishStepCallbackRef.set(otpFinishStepCallback);
        Intent intent = new Intent(context, OtpCodeInputActivity.class);
        if (otpException instanceof OtpValidationException) {
            intent.putExtra(IntentExtraFields.OTP_ERROR, "OTP value is incorrect, please try again.");
        }
        launcher.launch(intent);
    }

    public void onCreate(@NonNull LifecycleOwner owner) {
        launcher = registry.register(OTP_FINISH_STEP_KEY, owner,
                new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    OtpFinishStepCallback callback = otpFinishStepCallbackRef.get();
                    if (callback == null) {
                        return;
                    }

                    if (result.getResultCode() == Activity.RESULT_OK) {
                        Intent intent = result.getData();
                        if (intent != null) {
                            boolean shouldResend = intent.getBooleanExtra(
                                    IntentExtraFields.OTP_RESEND_FIELD, false);
                            if (shouldResend) {
                                callback.onOtpResend();
                            } else {
                                String code = intent.getStringExtra(
                                        IntentExtraFields.OTP_CODE_FIELD);
                                callback.onSuccess(new OtpFinishInput(code));
                            }
                        } else {
                            callback.onError();
                        }
                    } else {
                        callback.onError();
                    }
                });
    }
}

Verify the User Information

Once the user has made any edits to their pre-fill information, you should submit that information to the backend server so the Complete() call can then verify the user information.

// Send request to the backend to verify user information.
SendInfoResponse sendInfo(String firstName, String lastName) {
  YourBackendClient backend = new YourBackendClient(); // Backend API client
  
  // TODO: Build your SendInfoRequest object
  SendInfoRequest sendInfoRequest = new SendInfoRequest(firstName, lastName, ...);
                                                        
  // Send a sendInfoRequest to your backend server to get return user information.
  SendInfoResponse response = backend.verify(sendInfoRequest);
  
  // TODO: define your own SendInfoResponse object to parse the response
  return response;
}