OAuth in mobile apps: use PKCE, and stop shipping client secrets in the APK
Contents
Every few months I review a mobile app that ships an OAuth client secret inside the binary. Extracting a string from an APK or IPA takes about ten minutes, so that secret is public the day the app is published. Here is the setup that actually works for native apps.
Mobile apps are public clients
A public client is one that cannot keep a secret: native apps, single page apps, desktop apps. The OAuth specs say so explicitly, and the answer is not better obfuscation. The answer is a flow that does not need a secret: authorization code with PKCE.
How PKCE works
- The app generates a random
code_verifier(43 to 128 characters, from a cryptographically secure generator). - It computes
code_challenge = BASE64URL(SHA256(code_verifier))and sends the challenge withcode_challenge_method=S256in the authorization request. - The user logs in, and the authorization server redirects back to the app with a one-time code.
- The app exchanges the code for tokens and sends the original
code_verifier. The server hashes it and checks that it matches the challenge.
If another app on the device intercepts the redirect and steals the code, it cannot use it, because it does not have the verifier. That is the whole trick, and it is why the implicit flow is no longer recommended. The OAuth security best current practice tells you not to use implicit at all.
Use the system browser, not a WebView
Open the login page in the system browser component: ASWebAuthenticationSession on iOS, Custom Tabs on Android. Not an embedded WebView. In a WebView your app can read everything the user types, including their password for the identity provider, and the user cannot see a real address bar. Many providers block embedded WebViews for this reason. RFC 8252, OAuth for native apps, covers this in detail.
Store tokens in the platform keystore
Refresh tokens go in the iOS Keychain or in Android Keystore-backed storage. Not in AsyncStorage, not in plain SharedPreferences, not in a SQLite table. Use short-lived access tokens, and if your provider supports refresh token rotation, turn it on.
Checklist
- Authorization code + PKCE with S256. No implicit flow, no plain method.
- No client secret in the app. If your provider insists on one, your backend should do that part.
- System browser for login, never a WebView.
- Exact redirect URI matching on the server side.
- Tokens in Keychain or Keystore. Short access token lifetime, rotating refresh tokens.
None of this is new. But I still find the secret in the APK, so apparently it needs repeating.
Written by
Fatima Al-Sayed
Application security engineer at a bank in Dubai. Threat modelling, auth flows, secure code review. I will ask where your JWT secret lives. Arabic coffee enthusiast.
Get the best of #security-theatre weekly
One email each Monday with the most useful posts. No spam; unsubscribe in one click.
Comments
Sign in to join the conversation.
Sign in to comment