Credential stuffing is the automated replay of username and password pairs leaked from other services against your login. Against a mobile app, the bots rarely run your app at all: they read its traffic once, then call the login API directly from scripts at scale. Protecting a mobile app therefore means answering one question on every login request: did this come from a genuine copy of the app on a real device, and does the pattern look human? This article explains how to separate hostile automation from legitimate clients, where each decision belongs, and which policies keep real users signed in during an attack.
The abuse path: three kinds of client
Every login request that reaches your API comes from one of three places, and a good defence treats them differently.
- Automated requests. Scripts or bot frameworks call the login endpoint with leaked pairs, rotating IP addresses and device identifiers to look like many users.
- Invalid clients. A modified copy of your app, an emulator farm or an instrumented app driven by a hooking framework. The request looks like it came from your app because, in part, it did.
- Legitimate automation. Password managers filling fields, users with accessibility tools, your own test automation, and partners you have allowed to call the API.
Collect leaked pairs
Combination lists from unrelated breaches are loaded into a bot tool.
Learn the API
The attacker records the app's login call once, from a real or emulated device.
Replay at scale
Scripts send the call with thousands of pairs through rotating addresses.
Require app evidence
Requests without valid evidence from a genuine app are refused at the edge.
Decide per account
The backend weighs evidence, velocity and account history before allowing a login.
The distinction between the first two groups matters. Pure scripts are refused once the API demands evidence that only a genuine app on a real device can produce. Invalid clients are harder: they run your code, so the evidence must also say whether that code was modified, instrumented or running in an emulator.
Hostile automation
- No app evidence, or evidence replayed from another session
- Emulator, hooking framework or modified app copy
- Many accounts from one device profile
- Uniform timing, no screen interaction
Legitimate automation
- Evidence from a genuine app on a real device
- Password manager or accessibility service filling fields
- One user, a small number of devices
- Test and partner traffic from known, separately authenticated clients
Otukan attaches app integrity and runtime evidence to login requests from Android and iOS apps, and detects emulators, hooking frameworks and modified copies used to automate logins. Each platform produces its own evidence: Android offers the Play Integrity API and iOS offers App Attest, and Otukan treats them as separate sources.
OAT-008
OWASP automated threat identifier for credential stuffing
The enforcement boundary: app, gateway and backend
A common mistake is to ask one layer to do everything. The app cannot see traffic from scripts that never run it. The gateway sees volume but not accounts. The backend knows accounts but receives requests only after the gateway has let them through. Each layer should make the decisions it is best placed to make.
| Decision | App | Gateway | Backend |
|---|---|---|---|
| Produce evidence that the request came from a genuine app | Yes | ||
| Report emulator, hooking or modified app signals | Yes | ||
| Refuse requests with missing or invalid evidence | Yes | Yes | |
| Rate limit by address, device and endpoint | Yes | ||
| Rate limit failed logins per account | Yes | ||
| Check passwords against known breached lists | Yes | ||
| Step up with a second factor or challenge | Yes | Yes | |
| Lock, notify or reset an account | Yes |
Counts come from the enforcement table on this page. They show where the work sits, not how much traffic each layer stops.
The app’s role is narrow but essential: it is the only place that can produce evidence. That evidence is worth little unless something checks it, which is why the gateway and backend should both be able to refuse requests without it. For the gateway pattern in more depth, see WAF and API gateway signals.
Continuity: rate, challenge, retry and failure policies
Credential stuffing defences fail in two ways: they let the attack through, or they lock out real users. The second is easier to miss, because it looks like a working defence. Write the policies down before an attack, test them, and make sure support teams know them.
| Policy | What to decide | Example starting point |
|---|---|---|
| Rate | Limits per address, per device, per account and per endpoint | Tight per account, looser per address to allow shared networks |
| Challenge | When to ask for a second factor or a challenge | New device plus weak evidence, or unusual velocity |
| Retry | How the app backs off after a refusal | Honour the server’s retry header; never retry login automatically |
| Failure | What happens if the evidence service is unavailable | Fail open with extra scrutiny for known users, fail closed for new devices |
The retry policy deserves special attention. An app that retries failed logins on its own adds to the load during an attack and can trigger the same limits you set for bots. A standard response lets the app behave well:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 120
{"error": "rate_limited", "retry_after_seconds": 120}
For the protection itself, see credential stuffing protection.
Before the next attack
- Confirm that the login API refuses requests with no app evidence, on both Android and iOS.
- Give partner and test automation their own credentials and route, separate from app users.
- Set rate limits per account as well as per address, and test them from a shared network.
- Decide the fail-open or fail-closed rule for when the evidence service is unavailable.
- Make sure the app honours retry headers and never retries a login on its own.
- Check new and changed passwords against a breached-password list.