How to Test Two-Factor Authentication on iOS (Complete Guide)
How to Test Two-Factor Authentication on iOS (Complete Guide) starts with understanding why this security mechanism is a critical gatekeeper for user accounts and what failures look like in the wild.
How to Test Two-Factor Authentication on iOS (Complete Guide) starts with understanding why this security mechanism is a critical gatekeeper for user accounts and what failures look like in the wild. Two‑factor authentication (2FA) adds a second verification step—usually a time‑based one‑time password (TOTP), SMS code, or push‑notification approval—to the traditional username/password flow. On iOS, the implementation often lives inside a custom view controller that handles code entry, resend timers, and error states. When any part of this flow breaks, users can be locked out, attackers can bypass protection, or the app can violate App Store guidelines that require reliable authentication. Testing 2FA therefore touches functional correctness, security resilience, accessibility compliance, and user‑experience smoothness. The sections below give you a complete, practical playbook: a detailed test matrix, step‑by‑step manual procedures, automated strategies with Xcode‑based tools, real code snippets, common production pitfalls, accessibility and security checks, and how an autonomous, persona‑driven explorer like SUSA can surface issues that scripted tests miss.
How to Test Two-Factor Authentication on iOS (Complete Guide): Overview
Before diving into tactics, clarify the scope of what you will verify. A typical iOS 2FA screen contains:
- A field for the six‑digit code (often a secure UITextField with keyboard type .numberPad)
- A “Resend code” button that triggers a network request after a cooldown timer
- Links to “Use another method” (e.g., switch to SMS or authenticator app)
- Error handling for invalid codes, expired codes, network failures, and server‑side throttling
- Integration with the system auto‑fill (QuickType) for SMS codes when the user grants permission
- Accessibility labels and hints for VoiceOver
- Optional biometric fallback (Face ID/Touch ID) to approve a push‑based 2FA request
Your test plan must cover each of these touchpoints across happy paths, error paths, edge cases, and non‑functional dimensions. The following sections break the work into manageable pieces.
How to Test Two-Factor Authentication on iOS (Complete Guide): Test Matrix
A matrix helps you ensure coverage without duplication. Below is a comprehensive table that maps test scenarios to expected outcomes, required data, and automation feasibility. Each row is a distinct test case; you can copy it into a test‑management tool or spreadsheet.
| ID | Category | Description | Preconditions | Steps | Expected Result | Automation Feasibility |
|---|---|---|---|---|---|---|
| 1 | Happy Path – TOTP | User enters correct 6‑digit code from authenticator app | User is at 2FA screen, authenticator app shows valid code | 1. Tap code field 2. Paste or type code 3. Tap Verify | Success screen or next app flow appears | High (UI test) |
| 2 | Happy Path – SMS Auto‑fill | System offers QuickType suggestion for received SMS | Device has received SMS with code, user granted auto‑fill permission | 1. Wait for SMS notification 2. Tap code field 3. Select suggested code 4. Tap Verify | Code fills automatically, verification succeeds | Medium (requires mock SMS) |
| 3 | Happy Path – Push Approval | User receives push notification and taps Approve | Server sends push to registered device, user has Face ID enabled | 1. Receive push 2. Tap notification 3. Authenticate with Face ID 4. Confirm approval in app | Verification succeeds, proceeds to next step | Low (depends on push service) |
| 4 | Error – Invalid Code | User enters a wrong code | Same as 1 | 1. Enter incorrect 6‑digit value 2. Tap Verify | Inline error message appears, field retains focus, no network call | High |
| 5 | Error – Expired Code | User enters a code that server marks as expired | Server returns 410 Gone for code older than 30 s | 1. Wait for code to expire (or mock server) 2. Enter expired code 3. Tap Verify | Error indicates code expired, offers resend | Medium (needs time control) |
| 6 | Error – Network Failure | Verification request times out or returns 500 | Simulate network loss or server error | 1. Disable Wi‑Fi/cellular 2. Enter valid code 3. Tap Verify | App shows generic network error, offers retry, does not crash | High (using Network Link Conditioner) |
| 7 | Resend – Cooldown UI | Resend button is disabled during timer, shows remaining seconds | After sending code, timer starts | 1. Observe button state 2. Wait for timer to reach 0 3. Tap Resend | Button disabled, shows countdown; after 0 becomes enabled | High |
| 8 | Resend – Rate Limit | Server blocks resend after too many attempts | Server returns 429 Too Many Requests after N resends | 1. Tap Resend repeatedly until limit hit 2. Observe UI | App shows “Too many attempts, try later” and disables Resend | Medium (requires backend stub) |
| 9 | Accessibility – VoiceOver Labels | All elements have meaningful labels | VoiceOver running | 1. Enable VoiceOver 2. Swipe to each element 3. Listen to spoken hint | Each field/button announces purpose (e.g., “Enter verification code, text field”) | High (UI test with AX) |
| 10 | Accessibility – Dynamic Type | Text scales correctly with largest accessibility size | Set Dynamic Type to largest | 1. Open 2FA screen 2. Verify layout does not truncate or overlap | All labels and buttons readable, no clipping | Medium |
| 11 | Security – Code Exposure | Code never appears in logs or screenshots | Enable Xcode console capture, take screenshot | 1. Perform verification 2. Check console logs 3. Review screenshot | No plaintext code in logs; screenshot may be blurred if app uses secureTextField | Low (requires manual review) |
| 12 | Security – Brute Force Protection | Server throttles after repeated failed attempts | Backend configured to lock after 5 fails | 1. Enter wrong code 5 times 2. Attempt 6th | Account locked or delayed response, UI shows appropriate message | Low (needs backend) |
| 13 | Edge – Pasting from Clipboard | User pastes code from other app | Code copied to clipboard | 1. Copy code from Notes 2. Long‑press code field 3. Choose Paste 4. Tap Verify | Pasted code accepted, verification proceeds | High |
| 14 | Edge – Keyboard Types | Keyboard switches to numberPad, no extra characters | Default keyboard type set | 1. Tap code field 2. Verify keyboard shows only numbers | No letters or symbols appear | High |
| 15 | Edge – Interruption (Call/Switch) | Incoming call or app switch during verification | Receive phone call or press Home | 1. Start verification 2. Receive call 3. Return to app | Code field retains entered digits, timer continues (or pauses per spec) | Medium (requires UI interruption testing) |
| 16 | Edge – Low Memory | System sends memory warning while 2FA screen is active | Simulate low memory in Xcode | 1. Trigger memory warning 2. Continue interaction | App does not crash, state preserved | Low (requires XCTest with memory pressure) |
| 17 | Edge – Localization | All strings appear correctly in right‑to‑left language | Set device language to Arabic, region to Saudi Arabia | 1. Open 2FA screen 2. Verify layout mirrors, text reads RTL | Labels aligned right, inputs flow correctly | Medium |
| 18 | Edge – Dark Mode | UI adapts to dark appearance | Appearance set to Dark | 1. Switch to Dark Mode 2. Verify contrast, no invisible elements | All elements meet WCAG AA contrast, no color‑only info | High |
The matrix above gives you a concrete baseline. Adjust IDs to match your test‑case numbering scheme. In the next sections we’ll show how to execute each category manually, then how to automate the feasible ones.
How to Test Two-Factor Authentication on iOS (Complete Guide): Manual Testing Steps
Manual testing remains essential for exploratory checks, especially for edge cases that depend on timing, interruptions, or human perception. Follow this step‑by‑step routine for each build you receive.
- Environment Setup
- Install the latest build on a physical device (iOS 16 or newer) to capture real‑world sensor behavior (Face ID, ambient light).
- Enable Developer → Network Link Conditioner to simulate 3G, LTE, and offline states.
- Turn on Accessibility → VoiceOver and Switch Control to verify labels and hit‑targets.
- Set Debug → Capture View Hierarchy to inspect UI elements after each action.
- Happy Path Validation
- Launch the app, navigate to the login flow, and reach the 2FA screen.
- For TOTP: open your authenticator app, note the displayed code, enter it manually, and tap Verify. Confirm you land on the expected next screen (e.g., home dashboard).
- For SMS: request a code via the app’s “Send SMS” button, wait for the notification, tap the auto‑fill suggestion, and verify.
- For Push: ensure the device is registered for push, trigger a push from your test backend, approve via Face ID, and confirm success.
- Error Path Injection
- Invalid Code: type a clearly wrong sequence (e.g., 000000) and verify the inline error appears without a network call.
- Expired Code: adjust the device clock forward by 2 minutes (Settings → General → Date & Time → Set Automatically off) or use a mock server that returns a 410. Observe the expiration message.
- Network Failure: enable Airplane Mode, attempt verification, and ensure the app shows a retryable error and does not crash. Disable Airplane Mode and retry to confirm recovery.
- Rate‑Limited Resend: tap Resend rapidly until the backend returns 429; confirm the UI shows a throttling message and disables the button.
- Accessibility Checks
- With VoiceOver on, swipe across each element. Listen for spoken label, hint, and trait (e.g., “button”, “text field”).
- Increase Dynamic Type to the largest setting; ensure no text is truncated and buttons remain tappable (minimum 44 × 44 pt).
- Run the Accessibility Inspector (Xcode → Open Developer Tool → Accessibility Inspector) to audit contrast and missing traits.
- Security‑Focused Spot Checks
- Enable console logging (
os_logwith.default) and reproduce a successful verification. Search the log for the code string; it should not appear. - Take a screenshot while the code field is populated; verify the OS blurs secure fields if you marked the UITextField as
isSecureTextEntry = true. - Trigger Face ID cancellation and ensure the app does not reveal any partial code in error dialogs.
- Interruption and State Preservation
- While the timer is counting down, lock the device, receive a call, or switch to another app via the multitasking gesture. Return to the app and confirm the entered code remains, the timer continues from where it left off (or resets per your spec), and no data loss occurs.
- Simulate a low‑memory warning via Xcode’s Debug → Simulate Memory Warning; ensure the view controller does not deinit unexpectedly and that any transient state (e.g., timer) is restored from
viewDidLoadorviewWillAppear.
- Localization and Appearance
- Change language to a right‑to‑left locale (e.g., Arabic) and confirm the layout mirrors correctly.
- Toggle Appearance between Light and Dark; use the Accessibility Inspector’s Color Contrast tool to verify all foreground/background pairs meet at least 4.5:1 for normal text.
- Document Findings
- For each test case, record: build version, device model, iOS version, steps taken, observed result, expected result, and any attachments (screenshots, logs).
- Flag any deviation as a bug, assign severity, and add to your tracking system.
Following this manual routine gives you confidence that the core 2FA behavior works under realistic conditions. However, repeating these steps for every build is tedious; the next section shows how to automate the repeatable portions.
How to Test Two-Factor Authentication on iOS (Complete Guide): Automated Approaches
Automation shines for regression, CI pipelines, and scenarios that require precise timing or repeated inputs. On iOS, you have three primary options: XCTest/UI Testing, third‑party frameworks like EarlGrey or XCUITest with extensions, and device‑farms that support scripting via Appium. Below we detail how to automate each matrix category that is marked “High” or “Medium” feasibility.
Setting Up the Test Target
- Add a UI Testing target to your Xcode project (File → New → Target → UI Testing Bundle).
- In the generated
*UITests.swiftfile, import XCTest and configure theXCUIApplicationinstance. - Enable the
continueAfterFailure = falseflag if you want the suite to stop on the first failure, or keep it true to collect all issues in a single run.
Helper Extensions
Create a file UITestExtensions.swift with utilities that reduce boilerplate:
import XCTest
extension XCUIElement {
/// Taps the element after waiting for it to be hittable.
func safeTap() {
let exists = NSPredicate(format: "exists == true")
expectation(for: exists, evaluatedWith: self, handler: nil)
waitForExpectations(timeout: 10, handler: nil)
tap()
}
/// Enters a string into a text field, clearing any existing content.
func enterText(_ text: String) {
tap()
// Use double‑tap to select all, then delete
doubleTap()
typeText(XCUIKeyboardKey.delete.rawValue)
typeText(text)
}
}
These helpers make test steps readable and resilient to timing variations.
Automating Happy Paths
TOTP Verification
func testTOTPSuccess() {
let app = XCUIApplication()
app.launch()
// Assume navigation to login screen is encapsulated in helper methods
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
// Arrive at 2FA screen
let codeField = app.textFields["VerificationCode"]
codeField.enterText("123456") // Use a known static code from test authenticator
app.buttons["Verify"].tap()
// Assert next screen appears
XCTAssertTrue(app.staticTexts["Welcome"].exists, "Login should succeed")
}
Replace the static code with a value fetched from a test‑only authenticator server or a hardcoded seed that matches your test TOTP generator.
SMS Auto‑fill Simulation
Since real SMS cannot be triggered reliably in a UI test, mock the auto‑fill suggestion:
func testSMSAutofill() {
let app = XCUIApplication()
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
// Trigger the app to request an SMS code
app.buttons["SendCodeViaSMS"].tap()
// Simulate the system offering the code via pasteboard
UIPasteboard.general.string = "654321"
let codeField = app.textFields["VerificationCode"]
codeField.tap()
// Long press to bring up paste menu, then choose Paste
codeField.press(forDuration: 1.2)
app.menuItems["Paste"].tap()
app.buttons["Verify"].tap()
XCTAssertTrue(app.secureTextFields["WelcomeMessage"].exists)
}
This approach relies on the fact that iOS will show a paste option when the text field is tapped after a long press. Adjust timing if your UI uses a custom input accessory view.
Push Approval (Mock)
If your backend supports a test mode that returns a success response without real push, you can bypass the notification layer:
func testPushApprovalMock() {
let app = XCUIApplication()
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
// Assume the app shows a “Use Push” button
app.buttons["UsePush"].tap()
// Mock server returns success immediately; we just tap OK
app.buttons["OK"].tap()
XCTAssertTrue(app.staticTexts["AccountOverview"].exists)
}
If you need to test the actual push flow, integrate a local push notification server (e.g., NSPredicate‑based simulation using UNUserNotificationCenter) and trigger it from the test.
Automating Error Paths
Invalid Code
func testInvalidCodeShowsError() {
let app = XCUIApplication()
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
let codeField = app.textFields["VerificationCode"]
codeField.enterText("000000")
app.buttons["Verify"].tap()
let error = app.staticTexts["Invalid code. Please try again."]
XCTAssertTrue(error.exists)
// Ensure focus remains on code field
XCTAssertTrue(codeField.isFocused, "Code field should stay focused")
}
Network Failure
Use the Network Link Conditioner via a shell script before launching the test, or use URLProtocol stubbing inside the app for test builds:
func testNetworkFailure() {
let app = XCUIApplication()
// Launch with environment variable to activate stub
app.launchEnvironment["NETWORK_STUB"] = "failure"
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
let codeField = app.textFields["VerificationCode"]
codeField.enterText("123456")
app.buttons["Verify"].tap()
let retryAlert = app.alerts["Network Error"]
XCTAssertTrue(retryAlert.exists)
retryAlert.buttons["Retry"].tap()
// After retry, stub should succeed; adjust assertions accordingly
}
Inside your app’s networking layer, check for ProcessInfo.processInfo.environment["NETWORK_STUB"] and return a fabricated error or delay.
Automating Accessibility Checks
You can assert traits and labels directly:
func testAccessibilityLabels() {
let app = XCUIApplication()
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
let codeField = app.textFields["VerificationCode"]
XCTAssertEqual(codeField.label, "Enter verification code")
XCTAssertTrue(codeField.isEnabled)
let resendBtn = app.buttons["Resend code"]
XCTAssertEqual(resendBtn.label, "Resend code")
XCTAssertTrue(resendBtn.isEnabled)
}
For Dynamic Type, you can retrieve the font size and compare against the preferred size:
func testDynamicTypeLargest() {
let app = XCUIApplication()
app.launch()
// Set largest content size category via launch arguments
app.launchArguments += ["-UIContentSizeCategory", "UIContentSizeCategoryAccessibilityExtraExtraExtraLarge"]
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
let codeField = app.textFields["VerificationCode"]
let font = codeField.font
XCTAssertNotNil(font)
// Ensure the font point size is at least the system’s largest accessible size
XCTAssertGreaterThanOrEqual(font!.pointSize, UIFont.preferredFont(forTextStyle: .body).pointSize * 2.0)
}
Automating Localization and Appearance
Switch language and appearance via launch arguments:
func testArabicLayout() {
let app = XCUIApplication()
app.launchEnvironment["AppleLanguages"] = "(ar)"
app.launchEnvironment["AppleLocale"] = "ar_SA"
app.launch()
navigateToLogin()
// Check that a left‑aligned label in English becomes right‑aligned
let label = app.staticTexts["Enter verification code"]
XCTAssertTrue(label.frame.origin.x > app.frame.width / 2) // rough heuristic
}
For Dark Mode:
func testDarkModeContrast() {
let app = XCUIApplication()
app.launchArguments += ["-AppleInterfaceStyle", "Dark"]
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
let codeField = app.textFields["VerificationCode"]
// Use Accessibility Inspector via XCTAttributedString? Not directly available.
// Instead, assert that backgroundColor is dark and textColor is light.
XCTAssertTrue(codeField.backgroundColor?.isDark ?? false)
XCTAssertTrue(codeField.textColor?.isLight ?? false)
}
You may need extensions on UIColor to determine lightness/darkness via luminance formulas.
Test Data Management
For scenarios that depend on server state (e.g., rate‑limited resend, expired codes), consider embedding a lightweight test server in your app bundle (using GCDWebServer) or leveraging a feature flag that swaps the networking layer to a mock implementation when UITesting is detected. This keeps tests fast, deterministic, and independent of flaky network conditions.
Continuous Integration Integration
Add a step in your CI pipeline (e.g., GitHub Actions, Bitrise) that:
- Checks out the repository.
- Runs
xcodebuild test -project YourApp.xcodeproj -scheme YourAppUITests -destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' - Collects the test bundle’s JUnit report and publishes it.
- Optionally, runs a second job on a real device farm (BrowserStack, Firebase Test Lab) to validate device‑specific behavior like Face ID.
Automating the high‑feasibility rows from the matrix gives you rapid feedback on regressions, while manual exploratory testing covers the nuanced, timing‑dependent, and perception‑based cases.
How to Test Two-Factor Authentication on iOS (Complete Guide): Tooling and Code Examples
Beyond XCTest, several complementary tools can deepen your 2FA validation. Below is a curated list with short code or command snippets that show how to integrate each into your workflow.
| Tool | Purpose | iOS‑Specific Usage | Example Snippet |
|---|---|---|---|
| Network Link Conditioner | Simulate latency, packet loss, bandwidth limits | Preferable on device; can be toggled via Settings → Developer | sudo nlcfg -set profile "3G" (requires administrator) |
| Charles Proxy / mitmproxy | Intercept and modify HTTP(S) requests/responses | Install certificate on device, enable SSL proxying | In mitmproxy: request.headers["Authorization"] = "Bearer test-token" |
| Xcode Environment Variables | Inject test flags, switch mock servers | Set in Scheme → Arguments → Environment Variables | API_BASE_URL=https://mock.example.com |
| Simulator Shutdown / Boot | Test app behavior after device reboot | xcrun simctl shutdown then xcrun simctl boot | Useful for testing push token refresh |
| UI Recording (Xcode) | Generate boilerplate UI test code quickly | Perform actions in simulator while recording | Press the red record button in the test navigator |
| Accessibility Scanner (Android) – not iOS | N/A | Use Xcode’s Accessibility Inspector instead | N/A |
| SwiftLint | Enforce code style, avoid accidental logging of secrets | Add rule to reject print of variables named code or password | In .swiftlint.yml: regex: 'print\\(.*code.*\\)' |
| Security‑Focused Unit Tests | Validate that sensitive data never leaves the secure enclave | Test Keychain wrapper, ensure SecItemAdd returns success | `swift\nfunc testCodeNotStoredInUserDefaults() {\n let defaults = UserDefaults.standard\n XCTAssertNil(defaults.string(forKey: "lastVerificationCode"))\n}\n` |
| Fastlane Snapshot | Automate localized screenshot generation for App Store review | Run snapshot to produce screenshots in each language, then visually inspect 2FA screen | fastlane snapshot |
| Detox (Cross‑platform) | End‑to‑end testing on real devices/simulators with JavaScript DSL | Install Detox, write test in .js | `js\nawait element(by.id('verificationCode')).typeText('123456');\nawait element(by.id('verifyButton')).tap();\n` |
| Appium | Cross‑platform UI automation, useful if you already have Android scripts | Set up XCUITest driver, reuse locators | `java\nMobileElement code = driver.findElement(By.id(\"VerificationCode\"));\ncode.sendKeys(\"123456\");\n` |
Practical Example: Using mitmproxy to Force an Expired‑Code Response
- Install mitmproxy on your Mac (
brew install mitmproxy). - On the iOS device, install the mitmproxy certificate (Settings → General → VPN & Device Management → mitmproxy-ca-cert.pem).
- Enable HTTP Proxy on the Wi‑Fi network, pointing to your Mac’s IP and port 8080.
- Create a script
expired_code.py:
from mitmproxy import http
def response(flow: http.HTTPFlow) -> None:
if flow.request.pretty_url.endswith("/verify2fa"):
# Return 410 Gone to simulate expired code
flow.response = http.HTTPResponse.make(
410,
b'{"error":"code_expired"}',
{"Content-Type": "application/json"}
)
- Run mitmproxy with the script:
mitmproxy -s expired_code.py. - Launch your app, trigger a verification, and observe the UI handling the 410 response.
Practical Example: Validating SecureTextField via UI Test
func testCodeFieldIsSecure() {
let app = XCUIApplication()
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
let codeField = app.secureTextFields["VerificationCode"]
// Ensure the field is indeed a secure text field
XCTAssertTrue(codeField.exists)
// Attempt to read the value – should be nil
XCTAssertNil(codeField.value as? String)
}
If you accidentally bind a regular UITextField to the code input, the test will fail, catching a regression early.
Practical Example: Simulating Low Memory with XCTest
func testLowMemoryDoesNotCrash() {
let app = XCUIApplication()
app.launch()
navigateToLogin()
enterCredentials(username: "testuser", password: "Secret123")
// Send memory warning
XCUIDevice.shared.press(.lock) // lock then unlock to trigger warning in some cases
// Alternatively use private API (not recommended for App Store builds)
// For demonstration, we rely on system-generated warning via simulator:
// Hardware → Simulate Memory Warning
// After warning, continue interaction
let codeField = app.textFields["VerificationCode"]
codeField.enterText("123456")
app.buttons["Verify"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].exists)
}
In CI, you can use xcrun simctl spawn booted kill -s SIGUSR1 to send a memory warning signal to a specific process if you know its PID.
These tooling snippets give you concrete ways to reproduce the conditions outlined in the test matrix without resorting to guesswork.
How to Test Two-Factor Authentication on iOS (Complete Guide): Edge Cases and Production Pitfalls
Even with exhaustive matrix coverage, certain issues surface only after the app reaches real users. Below are the most common production‑only gotchas we have seen in iOS 2FA implementations, along with detection strategies.
1. Delayed Push Notification Due to Battery Optimizations
iOS may defer background delivery when the device is in Low Power Mode or when the app is background‑strict. Users report missing push‑based 2FA prompts, forcing fallback to SMS, which may be unavailable abroad.
Detection:
- Enable Low Power Mode in Settings → Battery.
- Trigger a push from your test backend while the app is backgrounded.
- Measure the latency between the server’s send timestamp and the arrival timestamp logged by
UNUserNotificationCenterDelegate. - Assert that the delay stays under a threshold (e.g., 5 seconds) or that the app shows a fallback UI after a configurable timeout.
Mitigation:
- Use
UNNotificationSettingto check authorization status and alert the user if push is disabled. - Implement a local timer that shows a “Didn’t receive the prompt? Try SMS” button after 8 seconds.
2. SIM‑Swap or Number‑Porting Attacks Affecting SMS Fallback
If your app allows users to switch from authenticator to SMS after a failed TOTP attempt, an attacker who has hijacked the phone number can receive the code.
Detection:
- In a staging environment, simulate a number change by updating the backend’s phone‑number field without re‑verifying the device.
- Attempt to request an SMS code; ensure the backend requires re‑authentication (e.g., password + device trust) before sending the SMS.
Mitigation:
- Bind SMS delivery to a device‑specific token (e.g., APNs token) that rotates on reinstall.
- Require re‑entry of the primary password before allowing an SMS fallback.
3. Clipboard Sharing Across Devices (Universal Clipboard) Leading to Code Leakage
Users may copy a code from a password manager on their Mac and paste it into the iOS app. If the app inadvertently logs the pasteboard contents or shares them via UIActivityViewController, the code could be exposed.
Detection:
- Enable the pasteboard logging entitlement (
NSPasteboardGenerallogging) in a debug build. - Copy a known test code from another app, paste into the 2FA field, then inspect the console for any
NSPasteboardread events beyond the expected paste.
Mitigation:
- Mark the code field as
secureTextEntry = trueto prevent the system from showing the content in the pasteboard preview. - Clear the pasteboard after a short interval using
UIPasteboard.general.string = nilintextFieldDidEndEditing.
4. Localization Breaking Layout on Long Languages
Languages like German or Finnish produce significantly longer strings. If you hard‑code widths or use fixed‑size containers, the “Resend code” button may overlap the timer label.
Detection:
- Run the UI test suite with
-AppleLanguages "(de)"and-AppleLanguages "(fi)". - Use Xcode’s Accessibility Inspector to check for clipped labels (
UILabel’snumberOfLinesset to 1 but text exceeding bounds). - Add a UI test that asserts each visible element’s frame is within the superview’s bounds.
Mitigation:
- Use Auto Layout with
leading,trailing,centerYconstraints and prioritize content hugging/compression resistance appropriately. - Set `label.line
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