How to Test Two-Factor Authentication on Android (Complete Guide)

Two‑factor authentication (2FA) is now a baseline security control for most Android applications that handle personal data, financial transactions, or enterprise credentials. When 2FA fails, attackers

January 16, 2026 · 14 min read · How-To Guides

Motivation

Two‑factor authentication (2FA) is now a baseline security control for most Android applications that handle personal data, financial transactions, or enterprise credentials. When 2FA fails, attackers can bypass the second layer and gain unauthorized access, while legitimate users may be locked out, leading to support spikes and churn. Testing 2FA is therefore not a niche activity; it is a core part of release validation because the flow touches multiple subsystems: UI, network, cryptography, background services, and inter‑app communication (e.g., authenticator apps or SMS retriever). A defect in any of these areas can manifest as a crash, an ANR, a silent fallback to password‑only login, or a leaked secret. The goal of this guide is to give you a repeatable, thorough method to uncover those defects before they reach users.

Test Matrix

Below is a comprehensive matrix that you can use as a checklist for each 2FA implementation you encounter. Rows represent test categories; columns represent the verdict you should record (PASS, FAIL, NA). Each cell expects a concrete observation (e.g., “OTP field accepts 6‑digit numeric input”, “Error toast shows after three wrong codes”).

CategorySub‑caseExpected behaviorPASSFAILNA
Happy pathCorrect OTP entered within validity periodLogin succeeds, session token issued, user lands on home screen
OTP retrieved via SMS Retriever APIAuto‑fill works, no manual entry needed
OTP from third‑party authenticator (TOTP)Manual entry works, QR code scanned correctly
Error pathsWrong OTP (incorrect) OTP entered, error toast/s
Expired OTP enteredError message “Code expired”
Network OTP request times outError message displayed, retry allowed, lockout after configurable attempts
OTP too short/longInput rejected immediately, validation feedback shown
Non‑numeric characters in numeric OTP fieldField rejects or sanitizes, no crash
Empty OTP submissionValidation prevents submission, focus remains on field
OTP request rate‑limitedUI shows “Too many attempts, try again later” and disables resend button
Edge casesDevice time changed manuallyApp detects time drift, either rejects OTP or shows warning
Airplane mode toggled during OTP waitApp handles lack of network gracefully, retains OTP state
App killed while waiting for OTPOn relaunch, OTP request is re‑initiated or user sees appropriate message
OTP received via push notification while app in backgroundNotification tap opens app and pre‑fills OTP
Biometric fallback offered after OTP failureBiometric prompt appears, succeeds if enrolled
AccessibilityTalkBack navigationAll OTP fields, resend button, and error messages are reachable and announced
Dynamic font scalingLayout does not break at 200% font size
Color contrastError text meets WCAG AA contrast ratio
Switch ControlOTP can be entered via switch scanning without missing characters
Security / PrivacyOTP leaked in logsNo OTP appears in Logcat or crash reports
OTP stored in plain textOTP never persisted to disk or SharedPreferences
Replay attackReusing a previously valid OTP is rejected
Brute‑force protectionAfter N failed attempts, OTP entry is temporarily disabled
Secret key exposureQR code or secret never appears in screenshots or accessibility buffer
InteroperabilitySMS Retriever API hash mismatchApp fails gracefully, falls back to manual entry
Authenticator app not installedApp shows clear instruction to install an authenticator
Multiple OTP channels (SMS + email)User can choose channel, each works independently

You can copy this table into a spreadsheet or test management tool and fill in the PASS/FAIL/NA columns for each build. The matrix forces you to verify not only the nominal flow but also the failure modes that often surface only under stress or unusual device states.

Manual Testing Approach

A disciplined manual session starts with a clean device state (no leftover accounts, cleared cache, and factory‑reset time). Follow these steps for each 2FA variant you need to validate:

  1. Preparation
  1. Baseline login
  1. SMS‑based OTP
  1. Authenticator‑app OTP (TOTP)
  1. Error injection
  1. Network interruptions
  1. Background and lifecycle
  1. Accessibility checks
  1. Security sniffing
  1. Post‑login verification

Document each observation in the test matrix. If any step fails, create a bug report that includes the exact device model, Android version, app version, and logs captured via adb bugreport.

Automated Approaches and Tooling Specific to Android

Manual testing is essential for exploratory work, but regression safety requires automation. Below are the most effective strategies for Android 2FA testing, ranging from unit‑level to UI‑level.

Unit‑level validation

Instrumented UI tests (Espresso)

Espresso runs on the device or emulator and can interact with native UI components. The following snippet demonstrates a happy‑path test for SMS Retriever‑based OTP:


@RunWith(AndroidJUnit4::class)
class OtpLoginTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun `sms retriever auto-fills and logs in`() {
        // Trigger OTP request
        onView(withId(R.id.btnRequestOtp)).perform(click())

        // Simulate SMS Retriever broadcast
        val intent = Intent(SmsRetriever.SMS_RETRIEVED_ACTION)
        intent.putExtra(SmsRetriever.EXTRA_SMS_MESSAGE, "<#> Your OTP is 123456 ABCDEF")
        InstrumentationRegistry.getInstrumentation()
            .targetContext
            .sendBroadcast(intent)

        // Verify OTP field filled
        onView(withId(R.id.etOtp)).check(matches(withText("123456")))

        // Submit
        onView(withId(R.id.btnSubmitOtp)).perform(click())

        // Assert landing on home screen
        onView(withId(R.id.nav_home)).check(matches(isDisplayed()))
    }
}

Key points:

Handling TOTP in Espresso

Testing TOTP requires generating a time‑based code within the test. The following utility computes a valid OTP given a shared secret:


fun generateTotp(secretBase32: String, timeStep: Long = 30): String {
    val crypto = Mac.getInstance("HmacSHA1")
    val key = Base32.decode(secretBase32)
    crypto.init(SecretKeySpec(key, "HmacSHA1"))
    val time = System.currentTimeMillis() / 1000 / timeStep
    val data = ByteBuffer.allocate(8).putLong(time.reversed()).array()
    val hash = crypto.doFinal(data)
    val offset = hash[hash.lastIndex] and 0x0f
    val truncated = ((hash[offset] and 0x7f) shl 24) or
                    ((hash[offset + 1] and 0xff) shl 16) or
                    ((hash[offset + 2] and 0xff) shl 8) or
                    (hash[offset + 3] and 0xff)
    val otp = truncated % 1000000
    return String.format("%06d", otp)
}

You can then call this function in your test, entering the result into the OTP field.

UI Automator for cross‑app flows

When 2FA involves switching to an authenticator app or handling a push notification, UI Automator is better suited because it can interact with any app on the device. Example: launching Google Authenticator, retrieving the code, and returning to the SUT.


@Test
public void totpViaAuthenticatorApp() throws UiObjectNotFoundException {
    // Assume we are on the OTP entry screen of the SUT
    UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

    // Tap the “Scan QR” button which launches Authenticator
    UiObject scanBtn = device.findObject(new UiSelector().resourceId("com.example.app:id/btnScanQr"));
    scanBtn.clickAndWaitForNewWindow();

    // In Authenticator, find the account label and extract the code
    UiObject account = device.findObject(new UiSelector()
            .textContains("example@example.com")
            .className("android.widget.TextView"));
    String otp = account.getText(); // assumes the app shows the code next to the label

    // Return to SUT (press back)
    device.pressBack();

    // Enter OTP and submit
    UiObject otpField = device.findObject(new UiSelector().resourceId("com.example.app:id/etOtp"));
    otpField.setText(otp);
    UiObject submit = device.findObject(new UiSelector().resourceId("com.example.app:id/btnSubmitOtp"));
    submit.click();

    // Verify home screen
    UiObject home = device.findObject(new UiSelector().resourceId("com.example.app:id/nav_home"));
    assertTrue(home.waitForExists(5000));
}

Appium for cross‑platform or hybrid apps

If your Android app uses a WebView for the 2FA screen (common in hybrid frameworks), Appium lets you switch contexts:


AndroidDriver<MobileElement> driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
// ... navigate to 2FA screen
Set<String> contexts = driver.getContextHandles();
for (String ctx : contexts) {
    if (ctx.contains("WEBVIEW")) {
        driver.context(ctx);
        WebElement otpInput = driver.findElement(By.id("otp-input"));
        otpInput.sendKeys("123456");
        driver.findElement(By.id("submit-btn")).click();
        driver.context("NATIVE_APP");
        break;
    }
}

Automated security checks

Continuous integration integration

Add the Espresso/UI Automator tests to your CI pipeline (GitHub Actions, GitLab CI, or Jenkins). Use Firebase Test Lab to run the instrumentation tests on a matrix of devices (different API levels, screen sizes, and locales). Example GitHub Actions snippet:


name: Android UI Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 30
          target: google_apis
          arch: x86_64
      - name: Run Espresso tests
        run: ./gradlew connectedAndroidTest

This setup guarantees that every commit is exercised against a fresh emulator with the same 2FA logic you validated manually.

Code Snippets and Commands You’ll Use Frequently

PurposeCommand / SnippetNotes
Install APK and clear dataadb install -r app.apk
adb shell pm clear com.example.app
Guarantees a clean state.
Simulate SMS Retriever broadcastadb shell am broadcast -a com.google.android.gms.auth.api.phone.SMS_RETRIEVED --es "com.google.android.gms.auth.api.phone.SMS_RETRIEVED_EXTRA" "<#> Your OTP is 654321 ABCD"Replace the OTP as needed.
Pull logs for OTP leakage`adb logcat -v threadtimegrep -i "otp\code"`Should return no matches.
Check shared preferences for plain OTPadb shell run-as com.example.app cat /data/data/com.example.app/shared_prefs/*.xmlLook for any containing digits.
Force time change (requires root)adb shell date 031512002023.00Sets date to Mar 15 2023 12:00:00. Use only on test devices.
Trigger push notification via FCM (test token)curl -X POST -H "Authorization: key=YOUR_SERVER_KEY" -H "Content-Type: application/json" -d '{"to":"DEVICE_TOKEN","notification":{"title":"OTP","body":"Your code is 789012"}}' https://fcm.googleapis.com/fcm/sendUseful for background‑notification scenarios.
Run all instrumented tests on Firebase Test Labgcloud firebase test android run --type instrumentation --app app-debug.apk --test app-debug-test.apk --device model=Pixel3,version=29,locale=en,orientation=portraitAdjust model/version as needed.
Generate TOTP in Bash (for manual verification)oathtool --totp -b SECRETBASE32Replace SECRETBASE32 with the authenticator secret.

Edge Cases That Only Appear in Production

Even with exhaustive matrix coverage, certain failure modes surface only when the app runs under real‑world conditions. Below are the most common production‑only 2FA bugs and how to detect them.

1. Carrier‑specific SMS filtering

Some mobile carriers (especially in regions with strict spam filters) delay or block SMS messages that contain certain patterns (e.g., “<#>”). If your app relies on the SMS Retriever API, the broadcast may never fire, leaving the user staring at a blank OTP field.

Detection:

2. Background execution limits on Android 12+

Starting with Android 12, background services face stricter start‑foreground limits. If your app starts a foreground service to listen for SMS Retriever intents while in the background, the system may kill it, causing OTP auto‑fill to fail after the app has been idle for a few minutes.

Detection:

3. Authenticator app time‑sync drift

Users may have their device clock off by several minutes due to manual time zone changes or poor NTP sync. Most authenticator apps tolerate a 30‑second window, but some implementations enforce a stricter window (e.g., 15 seconds). If your server validates with a larger window than the client, you’ll see intermittent “invalid OTP” reports from users whose clocks are off.

Detection:

4. Notification channel importance changes

If your app sends OTPs via push notification and the user has set the notification channel to “Importance: Low”, the heads‑up display may be suppressed, causing the user to miss the OTP.

Detection:

5. Biometric fallback race condition

Some apps allow biometric authentication after a failed OTP attempt. On certain devices, the biometric prompt can appear *before* the OTP error toast disappears, leading to overlapping UI and missed taps.

Detection:

6. Data‑saver mode stripping network payloads

When Android’s Data Saver is active, some apps downgrade network requests to reduce bandwidth. If your OTP verification endpoint relies on a POST with a JSON body, the body may be stripped, resulting in a 400 error that the app treats as a network failure rather than an invalid OTP.

Detection:

7. Shared user ID (UID) collisions in enterprise environments

In managed Android Enterprise profiles, multiple apps may share the same UID for sandboxing. If your app writes the OTP to a file using MODE_WORLD_READABLE (deprecated but still present in legacy code), a co‑resident app could read it.

Detection:

Addressing these issues often requires server‑side adjustments (wider time windows, idempotent OTP verification) or client‑side guards (checking notification importance, respecting background limits, using EncryptedSharedPreferences). Document any production‑only findings in a separate “Post‑release watchlist” so they can be regressed in future cycles.

Short Checklist for Release Sign‑off

Before you promote a build to production, run through this concise list. Mark each item as YES (verified) or NO (blocking).

If any item is NO, treat it as a blocker and investigate before proceeding.

Closing Takeaways

Testing two‑factor authentication on Android is not a checklist you can run once and forget. The flow straddles UI, networking, cryptography, and platform‑specific behaviors that evolve with each OS release. By combining a thorough test matrix, disciplined manual exploration, and layered automation (unit, Espresso, UI Automator, Appium), you gain confidence that both the nominal path and the myriad failure modes are covered.

Remember that production‑only bugs often stem from carrier filtering, background execution limits, notification settings, or device‑specific quirks that emulators do not replicate. Incorporate real‑device testing, especially with varied carriers and Android versions, and monitor logs for silent failures such as missing SMS Retriever broadcasts or leaked OTPs.

When you have the capacity, consider an autonomous, persona‑driven explorer like SUSA. It can wander through the app as a curious novice, an impatient power user, or an accessibility‑focused persona, exercising paths that scripted tests rarely touch—such as rapidly toggling airplane mode while the OTP dialog is visible, or attempting to paste a cloned OTP from another app’s notification. The insights from those exploratory runs frequently uncover edge cases that only appear under real‑world stress, complementing your deterministic test suites.

Finally, treat 2FA as a living contract between your client and your server. Any change to the OTP delivery mechanism, validity window, or backup authentication method should trigger a revisit of the matrix and the automation suite. By institutionalizing this practice, you reduce the risk of authentication‑related incidents and keep your users’ trust intact.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free