Blog · By Nimrics · 10 Sept 2026 · 5 min read
Securing a Mobile App's API: Keys, Tokens, Rate Limits and the OWASP API Top 10
Your mobile app is a public client — every secret inside it is already in the attacker's hands. How to design the API behind an app so that nothing important depends on the app keeping a secret.

I build Flutter apps for a living, and the security conversation with every new team starts with one sentence: the app is not a trusted place. Anything you ship inside it — an API key, a signing secret, a "hidden" endpoint — can be pulled out of the binary in an afternoon by someone with free tools and a spare phone. The app is a nice front door. The security lives behind it, in the API.
OWASP publishes an API Security Top 10, and most mobile-backend breaches map onto its first three entries. This post is the practical version: how to build an API for an app so that a determined user with a proxy and your decompiled APK still cannot do anything they should not.
1. Authentication: sessions the server controls
Use short-lived access tokens with refresh tokens. The app sends a username and password (or an OTP, or a social login) once and receives an access token that expires in minutes to an hour, plus a refresh token that expires in days to weeks. Access tokens are cheap to verify and short-lived, so a leaked one is a small problem; refresh tokens are stored more carefully and can be revoked on the server.
Store tokens in the platform's secure storage — Keychain on iOS, Keystore-backed storage on Android — never in shared preferences or a plain file.
Rotate refresh tokens on use and detect reuse. If a refresh token that was already exchanged is presented again, someone copied it: revoke the whole session family.
Log out means revoke. Logging out in the app must invalidate the refresh token on the server, not just delete it locally.
OTP by SMS is a weak factor. It is fine for sign-up friction, but rate-limit it hard (per phone number and per device and per IP), use six digits with a short expiry, and never return "code correct / incorrect" faster for one than the other.
2. Authorisation: check ownership on every request
This is the big one — Broken Object Level Authorization is number one in the OWASP list for a reason. The app asks for /orders/1234. The server must check not just that the caller is logged in, but that order 1234 belongs to them. Every list, every detail, every update, every delete.
The common failure is an endpoint that trusts an ID from the client: change 1234 to 1235 in a proxy and read someone else's order, invoice or chat. There is no clever fix; it is a rule. Every handler that touches a resource loads it scoped to the authenticated user. In practice, that means the database query includes WHERE user_id = :current_user — or a policy layer that does it for you — and there is a test that proves it for every endpoint.
Also watch function-level authorisation: admin endpoints must check an admin role on the server. Hiding the admin screen in the app is not a control.
3. Rate limiting: assume the client is a script
Because the app can be automated, every endpoint needs a limit:
- Authentication and OTP endpoints: tight limits per account, per device and per IP, with exponential back-off.
- Search and listing endpoints: limits per user, plus caps on page size.
- Expensive endpoints (image processing, reports, anything that calls a paid third party): a quota per user per day.
Return 429 Too Many Requests with a Retry-After header. Do rate limiting at the edge (API gateway, reverse proxy) so it is not something each developer has to remember.
4. Input validation and mass assignment
Validate every field on the server: type, length, range, allowed values. Reject unknown fields rather than storing them. Mass assignment — where the client sends "is_admin": true or "price": 0 and the framework helpfully saves it — is still common in frameworks that bind request bodies straight to models. Use explicit allow-lists for writable fields.
5. Do not leak data the screen does not show
APIs often return whole records because it is easy, and the app just shows two fields. The other twenty are still in the response, visible to anyone with a proxy. Return exactly what the screen needs; define response shapes per endpoint. Strip internal IDs, other users' details, cost fields and anything a competitor would enjoy.
6. Secrets: there are none in the app
- Third-party API keys (maps, payments, SMS, AI services) do not go in the app. The app calls your API; your API calls the third party with a key that lives on the server.
- If a client-side key is unavoidable (some map SDKs), restrict it in the provider's console to your app's package name and signing certificate, and to the minimum APIs.
- Signing the request with a secret embedded in the app buys nothing against a reverse engineer. It is fine as a nuisance for casual scrapers, but never treat it as authentication.
7. Transport: TLS, and pinning only if you can maintain it
Everything over HTTPS, obviously. Certificate pinning makes interception harder but breaks the app when certificates rotate unless you pin carefully (to a public key or an intermediate, with a backup pin) and can ship updates fast. For most consumer apps, strong TLS plus the server-side controls above is the better trade; add pinning for high-value flows if you have the operational discipline.
8. App-side hygiene
- Obfuscate and minify release builds; it raises the cost of casual reverse engineering.
- Disable debug logging in release; token values in logs are a classic leak.
- Use platform attestation (Play Integrity, App Attest) if you want the server to know the request came from an unmodified app — useful, but it is a signal, not a guarantee.
- Keep dependencies current; a vulnerable networking or crypto library in the app is as bad as one on the server.
9. Monitoring the API
Log every authenticated request with user ID, endpoint, status and latency. Alert on: authentication failures spiking, one user hitting many other users' resource IDs (the fingerprint of an authorisation bypass attempt), and traffic from unusual regions or user agents. This is also how you find the bug that the tests missed.
The one-line test
Before shipping an endpoint, ask: if a hostile user sends this request with arbitrary parameters from a script, with a valid login for a different account, what happens? If the answer is anything other than "they get a 403 or their own data", it is not done. An app can be beautiful and fast; the API is where it is either secure or it is not.