How to Debug Broken Authentication in Mobile Apps
Debugging broken authentication starts with reproducing the failure reliably, then isolating whether the problem lives in credential handling, token validation, session management, or backend communic
How to Debug Broken Authentication in Mobile Apps
Debugging broken authentication starts with reproducing the failure reliably, then isolating whether the problem lives in credential handling, token validation, session management, or backend communication. The following guide walks you through a reproducible workflow, the signals to collect, the tools that expose those signals, and concrete fixes for the most common root causes.
---
How to Debug Broken Authentication in Mobile Apps: Understanding the Vulnerability Surface
What “broken authentication” means in practice
Authentication flaws appear when an app lets an attacker bypass login, reuse a stale token, or extract credentials through side‑channels. OWASP Mobile Top 10 lists them under M2: “Insecure Authentication”. Typical symptoms include:
- Successful login with incorrect or empty credentials.
- Ability to replay an old access token after logout.
- Session cookies or JWTs stored in plain text or world‑readable files.
- Missing multi‑factor verification for high‑risk actions.
- Token leakage via logs, crash dumps, or clipboard.
Primary root‑cause categories
| Category | Typical code pattern | What goes wrong |
|---|---|---|
| Credential validation | if (username.equals(input) && password.equals(input)) { … } | Uses weak comparison, hard‑coded values, or trusts client‑side hash. |
| Token issuance | String token = JWT.create().withClaim("sub", userId).sign(algorithm); | Uses weak algorithm (none/HMAC with exposed key) or omits expiration. |
| Session storage | SharedPreferences.putString("auth_token", token); | Stores token in mode MODE_WORLD_READABLE. |
| Token renewal | if (token.isExpired()) refreshToken(); | Refresh endpoint does not bind token to device or user. |
| Transport security | HttpURLConnection.setSSLSocketFactory(factory); | Accepts self‑signed certificates or disables hostname verification. |
| Multi‑factor bypass | if (mfaRequired) { showMfaScreen(); } else { grantAccess(); } | Logic error lets mfaRequired stay false after a failed challenge. |
Understanding these buckets helps you focus logs, breakpoints, and network filters on the relevant code paths.
---
How to Debug Broken Authentication in Mobile Apps: Building a Reproducible Test Harness
Device or emulator preparation
- Reset state –
adb shell pm clear com.example.appremoves SharedPreferences, databases, and cache. - Enable debugging – In Developer Options turn on “USB debugging” and “Verify apps over USB”.
- Install a trust store – If you plan to intercept TLS, install your proxy’s CA certificate on the device (
adb push mitmproxy-ca-cert.cer /sdcard/then Settings → Security → Install from storage).
Test matrix for authentication flows
| Flow step | Input | Expected outcome | Negative test | Pass/Fail criteria |
|---|---|---|---|---|
| Launch app | – | Shows login screen | – | UI renders within 2 s |
| Enter valid creds | user1 / P@ssw0rd! | Auth token received, home screen shown | – | Token ≠ null, expiration > now |
| Enter invalid creds | bad / bad | Login error toast | – | No token, error message displayed |
| Empty fields | ` / ` | Validation error | – | No network request, UI shows “required” |
| SQLi attempt | ' OR 1=1-- / any | Rejected by backend | – | Backend returns 401, no token |
| Token replay | Use token from previous valid session after logout | Access denied (401) | Re‑use token | Request to protected endpoint returns 401 |
| Token tampering | Flip a bit in JWT signature | Signature verification fails | Modify token | Backend returns 401 |
| MFA bypass | Submit login, then directly hit /home endpoint | Blocked, redirected to MFA | Skip MFA screen | Request returns 403 or redirect to MFA |
| Credential leakage | Login, then inspect logcat / file system | No plain‑text password appears | grep for password | No matches in logcat or /data/data/*/files |
Execute each row, capture the device log (adb logcat -v threadtime > auth_debug.log) and network trace (see next section). A single step that deviates from the expected outcome flags a candidate defect.
---
How to Debug Broken Authentication in Mobile Apps: Logging and Tracing Strategies
Enabling verbose auth‑module logs
Most mobile SDKs expose a logger you can switch at runtime. For Android, add this to your Application.onCreate():
if (BuildConfig.DEBUG) {
Logger.getLogger("com.example.auth").setLevel(Level.FINE);
Logger.getLogger("org.apache.http.wire").setLevel(Level.FINE);
}
For iOS (Swift) you can set:
#if DEBUG
AuthLogger.shared.logLevel = .verbose
#endif
These statements dump:
- Username/password as they enter the validation method (mask them in production, but keep raw for debugging).
- The exact JWT or opaque token string returned from the token endpoint.
- Header
Authorization: Beareron each subsequent request. - Response codes and bodies from
/refresh,/revoke, and protected endpoints.
Adding correlation IDs
When you instrument the login flow, generate a UUID at the start of the flow and attach it to every log line:
String flowId = UUID.randomUUID().toString();
Logger.getLogger("com.example.auth").info("[{}] Starting login", flowId);
// later
Logger.getLogger("com.example.auth").info("[{}] Token received: {}", flowId, token);
Correlation lets you grep the massive logcat output for a single attempt:
adb logcat | grep \"[$flowId]\"
Stack‑trace capture on exceptions
Wrap authentication calls in a try/catch that logs the full stack:
try {
val token = authService.login(username, password)
} catch (e: Exception) {
Logger.getLogger("com.example.auth").error("Login failed", e)
throw e // rethrow so UI still shows error
}
If a NullPointerException occurs inside the token‑creation library, the stack will reveal whether the failure is due to a missing secret key or a malformed claim.
---
How to Debug Broken Authentication in Mobile Apps: Using Network Proxies and Packet Captures
Setting up mitmproxy (or Charles)
- Install mitmproxy on your workstation:
pip install mitmproxy. - Launch it on port 8080:
mitmproxy --mode transparent --showhost. - On the device, configure Wi‑Fi to point to the host’s IP and port 8080 (manual proxy).
- Install the mitmproxy CA certificate as described earlier.
Filtering auth‑related traffic
In mitmproxy’s console view press f to add a filter:
~u /oauth/token || ~u /api/v1/auth || ~h "Authorization: Bearer"
This shows only token requests, refresh calls, and any request that carries an Authorization header.
Inspecting request/response details
Select a request and press Enter to see:
- Request line – method, URL, HTTP version.
- Headers – look for
Content-Type: application/json,Accept: application/json, and cruciallyAuthorization. - Body – JSON payload containing
grant_type,username,password,refresh_token, etc. - Response – status code,
Set-Cookieheaders, JWT in body,expires_infield.
If you see a 200 OK response to a request with "username":"" or "password":"" you have identified a credential‑validation bypass.
Detecting token leakage
Search the response bodies for the token string in places it shouldn’t appear:
~b "eyJ" // matches JWT start
If the token appears in a Location header redirect or in a JSON field named debug_token, you have a leakage vector.
Capturing raw TLS with tcpdump (for low‑level analysis)
When you suspect certificate pinning failures, run on the device (requires root):
adb shell tcpdump -i any -s 0 -w /sdcard/auth.pcap port 443
Then pull the file: adb pull /sdcard/auth.pcap . and open in Wireshark. Apply the display filter http2 || ssl.handshake.type == 1 to see the ClientHello. If the server sends a certificate that is not pinned, you’ll see the server’s cert chain in the clear—useful for confirming pinning bypass.
---
How to Debug Broken Authentication in Mobile Apps: Profiling and Runtime Analysis
Using Android Studio Profiler for auth‑related CPU spikes
- Run the app from Android Studio with profiling enabled.
- Navigate to the CPU tab, start recording, perform a login attempt, stop.
- Look for methods in
com.example.auth.*that consume > 30 % of the CPU during the token‑generation step.
- A spike in
MessageDigest.updatemay indicate a weak hash (e.g., MD5) being used for password hashing. - Excessive time in
Base64.encodecould point to unnecessary encoding of secrets before storage.
Tracking object allocations with the Memory Profiler
During a login flow, watch for large allocations of byte[] or String that retain the password after the method returns. If you see a char[] lingering in the heap, the app is likely storing the password in plain text (perhaps for “remember me”).
Instrumenting with Frida for runtime inspection
Frida lets you inject JavaScript into a running process without recompiling. Example script to intercept the login method:
Java.perform(function () {
var AuthManager = Java.use('com.example.auth.AuthManager');
AuthManager.login.overload('java.lang.String', 'java.lang.String').implementation = function (user, pass) {
console.log('[Auth] login called with user: ' + user + ', pass: ' + pass);
var result = this.login(user, pass);
console.log('[Auth] token returned: ' + result);
return result;
};
});
Run with:
frida -U -f com.example.app -l auth_trace.js --no-pause
The console output gives you the exact credentials passed in and the token returned, even if the app obfuscates them in release builds.
Detecting insecure storage with MobSF
Run the Mobile Security Framework (MobSF) static analysis on the APK:
docker run -it --rm -v $(pwd):/src opensecurity/mobsf:latest \
python /opt/mobsf/run.py apkscan -f /src/app.apk
MobSF will flag:
MODE_WORLD_READABLEon SharedPreferences files.- Hard‑coded AES keys in
res/values/strings.xml. - Use of
android:allowBackup="true"exposing data via adb backup.
---
How to Debug Broken Authentication in Mobile Apps: Manual Debugging Workflow
Step 1 – Isolate the failure mode
From the test matrix, pick the first failing step (e.g., “login succeeds with empty password”). Note the exact UI interaction and the expected network call.
Step 2 – Capture a clean log set
adb logcat -c # clear buffer
adb logcat -v threadtime > clean.log
Perform the failing action, then stop logging (Ctrl+C).
Step 3 – Grep for the correlation ID
If you injected a UUID, find it:
grep "\[a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8\]" clean.log > flow.log
Step 4 – Examine the log flow
Read flow.log top‑to‑bottom. Look for:
- Any
WARNorERRORlines preceding the token receipt. - Calls to validation methods that return
nullorfalsebut are ignored. - Missing checks like
if (token == null) { throw new AuthException(); }.
Step 5 – Set breakpoints
In Android Studio, place a breakpoint at the start of the validation method (AuthValidator.checkCredentials). Debug the app, step over each line, and watch the variables:
- Does the method receive empty strings?
- Is there a early
return true;before the password comparison?
Step 6 – Verify network payload
In mitmproxy, find the corresponding request. Confirm whether the payload actually contains the empty fields you sent. If the client side is sending correct data but the server still returns a token, the bug is server‑side; otherwise it’s client‑side logic.
Step 7 – Validate token contents
Decode the JWT (using jwt.io or the command line):
echo "<token>" | cut -d'.' -f1,2 | base64 -d | jq .
Check the exp, nbf, aud, and iss claims. If exp is far in the future or missing, the token issuance logic is flawed.
Step 8 – Re‑test with a fix
Apply a minimal code change (e.g., add if (TextUtils.isEmpty(password)) return false;), rebuild, reinstall (adb install -r app-debug.apk), and repeat steps 2‑5. The flow should now fail as expected.
---
How to Debug Broken Authentication in Mobile Apps: Automated Detection with Autonomous Exploration (SUSA)
How SUSA surfaces authentication gaps
When you point SUSA at an APK or a web URL, its exploration engine builds a state‑machine of screens, inputs, and dialogs. For each discovered login‑like screen it automatically:
- Generates a matrix of credential combinations (valid, invalid, empty, SQLi, long strings).
- Sends each combination to the backend while recording responses, logs, and network traces.
- Flags any attempt that results in a successful authenticated state (e.g., navigation to a home screen, receipt of a non‑expired token) despite invalid input.
- Checks token storage locations (SharedPreferences, Keychain, file system) for plain‑text secrets after each flow.
- Verifies that protected endpoints reject requests with stale or tampered tokens.
Because SUSA drives the app with varied personas (e.g., “impatient” who spam the login button, “adversarial” who inject special characters), it often hits edge cases that manual testers miss, such as a race condition where a rapid double‑tap bypasses a loading spinner and sends a second request before the first token is invalidated.
Running SUSA from the CLI
pip install susatest-agent
susatest run \
--apk ./app-release.apk \
--personas curious impatient adversarial \
--output ./susatest-report.json \
--log-level debug
The CLI exits with a non‑zero code if any authentication‑related finding is marked FAIL. The JSON report contains:
findings[].type→AUTH_BYPASS,TOKEN_LEAK,WEAK_TOKEN,SESSION_FIXATION.findings[].steps→ exact UI interaction sequence (e.g., “tap email field → type ‘’ → tap password field → type ‘’ → tap login”).findings[].evidence→ screenshots, log snippets, request/response bodies.
You can feed this report into your CI pipeline:
if ! susatest run --apk ./app.apk --fail-on-auth; then
echo "Authentication regression detected"
exit 1
fi
Limitations to keep in mind
- SUSA explores the UI layer; it cannot see inside native cryptographic libraries unless those libraries expose observable side‑effects (e.g., writing a token to SharedPreferences).
- It relies on the app being instrumentable (debuggable or with Frida‑compatible runtime). For fully obfuscated release builds you may need to re‑sign with a debuggable variant for accurate findings.
- The tool does not replace backend‑focused scanners (e.g., OWASP ZAP) but complements them by validating that the client correctly enforces the contract.
---
How to Debug Broken Authentication in Mobile Apps: Fixing Common Root Causes
1. Weak or missing credential validation
Problem: The login method returns true if either username *or* password matches a hard‑coded value, or it trims input before comparison, allowing SQL‑style injection.
Fix:
public boolean validateCredentials(String user, String pass) {
if (user == null || pass == null) return false;
if (user.isEmpty() || pass.isEmpty()) return false;
// Use prepared statements or parameterized queries on the backend
return authRepository.checkUser(user, pass); // backend does real verification
}
Add unit tests that attempt empty strings, whitespace-only strings, and Unicode homoglyphs.
2. Token issued without expiration or with weak algorithm
Problem: JWT created with Algorithm.none() or HS256 using a key that is embedded in the APK.
Fix:
- Switch to RS256 with a private key kept only on the server.
- Always set an expiration (
exp) claim not exceeding 15 minutes for access tokens; use refresh tokens for longer sessions. - Store the verification public key in the app’s resources only for validation, never for signing.
// Server‑side (Java/JJWT)
RSAPrivateKey privateKey = KeyUtil.loadPrivateKey("private.pem");
String token = Jwts.builder()
.setSubject(userId)
.setIssuedAt(now)
.setExpiration(Date.from(now.plusMinutes(15)))
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
3. Insecure token storage
Problem: SharedPreferences.putString("jwt", token); with default mode (MODE_PRIVATE is okay, but if the app mistakenly uses MODE_WORLD_READABLE or writes to external storage).
Fix:
- Keep the mode explicit:
getSharedPreferences("auth", Context.MODE_PRIVATE). - On Android 9+ use
EncryptedSharedPreferencesfrom Jetpack Security:
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
EncryptedSharedPreferences encryptedPrefs = EncryptedSharedPreferences.create(
context,
"encrypted_auth_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
encryptedPrefs.edit().putString("jwt", token).apply();
- On iOS, store tokens in the Keychain with
kSecAttrAccessibleWhenUnlockedThisDeviceOnly.
4. Missing token binding to device or session
Problem: Refresh endpoint accepts any valid refresh token, enabling token theft across devices.
Fix:
- Include a device‑specific claim (e.g., a UUID generated on first install and stored in the Keystore/Keychain) inside the refresh token.
- Server validates that the claim matches the device identifier sent in the
X-Device-Idheader.
{
"sub": "user123",
"device_id": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"exp": 1735689600
}
5. Insufficient transport security
Problem: App uses OkHttpClient.Builder().certificatePinner(null) disabling pinning, or accepts all SSL certificates via X509TrustManager.
Fix:
- Enable certificate pinning with the server’s leaf certificate or SPKI hash.
CertificatePinner pinner = new CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(pinner)
.build();
- If you must use a custom trust store for internal PKI, load it explicitly and do not fall back to the system trust store.
6. Multi‑factor authentication bypass via state confusion
Problem: After a failed MFA challenge, the app still sets isAuthenticated = true because the flag was set earlier in the flow.
Fix:
- Use a state machine:
LOGIN_STARTED → CREDENTIALS_VALIDATED → MFA_PENDING → AUTHENTICATED. - Only transition to
AUTHENTICATEDafter the MFA verification call returns success. - Clear any temporary authentication flags on logout or navigation away from the login flow.
---
How to Debug Broken Authentication in Mobile Apps: Preventive Practices and Checklist
Checklist for each release
| ✅ Item | How to verify |
|---|---|
| Credential fields reject empty/whitespace only | Unit test with assertFalse(validator.validate("", "pwd")) |
| Password never logged or stored in plain text | grep -R "password" in source; run MobSF report; inspect logcat after login |
Access token has exp ≤ 15 min, refresh token has longer life + device binding | Decode JWTs from mitmproxy; verify claims |
| Token stored in EncryptedSharedPreferences / Keychain | Check code for EncryptedSharedPreferences or Keychain usage |
| Certificate pinning active for all network domains | Run ./gradlew :app:assembleDebug then inspect generated network_security_config.xml or OkHttp client |
| MFA gate cannot be bypassed by direct navigation to protected UI | Attempt to launch home screen via adb shell am start -n com.example.app/.HomeActivity without completing MFA |
| Refresh endpoint validates device‑id claim | Send a refresh token from another device; expect 401 |
| Error messages do not reveal whether username or password is wrong | Ensure login failure toast is generic (“Invalid credentials”) |
| No hard‑coded cryptographic keys in APK | Run grep -r "BEGIN PRIVATE KEY" on unpacked APK; also run MobSF secret‑scan |
| App clears auth state on logout / device reboot | After logout, verify SharedPreferences no longer contains token; after reboot, confirm login screen appears |
CI integration
Add a step that runs SUSA’s authentication persona matrix and fails the build on any AUTH_BYPASS or TOKEN_LEAK finding. Example GitHub Actions snippet:
- name: Install SUSA
run: pip install susatest-agent
- name: Run Auth Checks
run: |
susatest run \
--apk ./app/build/outputs/apk/release/app-release.apk \
--personas curious impatient adversarial \
--fail-on-auth \
--output auth-report.json
Runtime defenses
- Enable StrictMode to catch accidental disk reads/writes of auth data on the main thread (helps spot plain‑text writes).
- Use Network Security Configuration to block clear‑text traffic (
). - Turn on Firebase App Check or equivalent to ensure only your genuine client can talk to the backend.
---
How to Debug Broken Authentication in Mobile Apps: Real‑World Examples
Example 1 – Empty‑password bypass in a banking app
Symptom: Users could log in with any username and a blank password, gaining access to account balances.
Root cause: The validation method used if (password.isEmpty()) return true; to “avoid null‑pointer exceptions”.
Discovery:
- Test matrix step “Empty fields” returned HOME screen.
- Logcat showed
[Auth] login called with user: 'alice', pass: ''followed by[Auth] Token issued. - Mitmproxy request body:
{"username":"alice","password":""}.
Fix: Removed the early return, added if (TextUtils.isEmpty(password)) return false;. Regression test added to CI.
Example 2 – JWT with none algorithm accepted
Symptom: Attackers could forge admin tokens by signing with the none algorithm.
Root cause: Server-side JWT library allowed Algorithm.none() when no explicit algorithm was enforced.
Discovery:
- SUSA’s adversarial persona sent a token with header
{"alg":"none","typ":"JWT"}. - Backend responded
200 OKand granted admin role. - Token decoded showed
"admin":true.
Fix: Configured the JWT verifier to reject any token lacking a valid signature (require(HMAC256(secret)) or require(RSA256(publicKey))). Added unit test that attempts to forge a none‑algorithm token and expects 401.
Example 3 – Token leakage via Android backup
Symptom: Rooted users could extract the auth token via adb backup and reuse it on another device.
Root cause: The app stored the JWT in a SharedPreferences file named auth_prefs.xml with MODE_PRIVATE, but had android:allowBackup="true" in the manifest.
Discovery:
- After login, ran
adb backup -f backup.ab com.example.app. - Restored on a different device with
adb restore backup.ab; the app launched straight to the home screen without prompting login. - Extracted
auth_prefs.xmlshowed.eyJ...
Fix: Set android:allowBackup="false" in the manifest and added a backup agent that clears auth data before backup. Also migrated to EncryptedSharedPreferences.
Example 4 – MFA bypass through rapid double‑tap
Symptom: Tapping the login button twice in quick succession allowed users to skip the MFA screen.
Root cause: The UI disabled the button after the first click, but a race condition let the second click invoke the login API before the first request completed, causing the server to issue a session token that the client treated as fully authenticated.
Discovery:
- Added correlation ID logs; two login calls with same UUID appeared within 80 ms.
- The second call’s response contained a token and the UI navigated to home before the MFA challenge fragment was shown.
Fix: Disabled the button and set a flag loginInProgress = true that is cleared only after *both* credential verification and MFA verification complete. Also disabled the screen via window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, ...) while waiting for server response.
---
How to Debug Broken Authentication in Mobile Apps: Closing Takeaways
- Reproduce first: Use a deterministic test matrix that covers valid, invalid, empty, and malformed inputs, plus token‑replay and tampering scenarios.
- Collect correlated evidence: Enable verbose auth logs with a unique flow ID, capture mitmproxy traffic, and, if needed, pull a pcap for TLS introspection.
- Isolate the layer: Determine whether the fault lives in client‑side validation, token issuance/storage, transport security, or server‑side enforcement.
- Leverage tooling: Android Studio profilers, Frida, MobSF, and autonomous explorers like SUSA each shine at different stages—use them in combination.
- Fix with defensive defaults: Treat empty credentials as invalid, enforce short-lived signed tokens with device binding, store secrets in encrypted containers, pin certificates, and make MFA a hard gate.
- Guard against regression: Automate the matrix and persona‑based checks in CI; treat any authentication‑related finding as a blocker.
- Remember the human factor: Personas such as “impatient” or “adversarial” expose timing and edge‑case bugs that functional tests miss.
By treating authentication as a state‑machine with clear entry and exit criteria, instrumenting every transition with logs and correlation, and validating both the client and server sides of the contract, you turn a nebulous “login sometimes works” issue into a deterministic, repeatable debugging cycle that yields reliable fixes and prevents future regressions.
---
*End of article.*
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