Mobile Auth Done Right: OAuth, Sessions, and Sign in with Apple
Authentication is where security architecture meets first impressions: it's the first screen many users see, the gate every feature sits behind, and the code most likely to be quietly wrong underneath — wrong in ways that don't show up in testing, because auth bugs are disproportionately about edge cases, expiry, and adversaries. The encouraging part is that mobile auth is a genuinely solved problem with actual standards behind it, written by people who have watched every mistake happen at scale. Most auth mistakes in shipped apps come not from difficulty but from not knowing the standards exist. This post is a tour of the ones that matter.
Social login: the browser, not a WebView
When you add "Sign in with Google" or any OAuth-based login, the temptation is to open the provider's page inside an embedded WebView — it keeps the user "in the app" and feels more polished. Don't. And this isn't style advice; it's written down: RFC 8252 (OAuth 2.0 for Native Apps) is the IETF's best-current-practice document for exactly this situation, and it says native apps must use an external user agent — the system browser — rather than an embedded WebView, paired with PKCE (Proof Key for Code Exchange) to protect the authorization flow against interception.
The reasons are practical, not ceremonial. In a WebView, your app controls the page — it can read keystrokes, inject JavaScript, harvest the user's Google or Facebook password — which is exactly the trust model OAuth exists to prevent. Users can't verify the URL bar because there isn't one. Providers know all this, and increasingly detect and block WebView logins outright, so the shortcut also just breaks. Meanwhile the system browser brings a gift: it shares the user's existing sessions, so "Sign in with Google" becomes one tap of an account chooser instead of a password prompt on a phone keyboard.
On iOS the right tool is ASWebAuthenticationSession; on Android, Chrome Custom Tabs. In Flutter you rarely touch these directly — packages like google_sign_in, sign_in_with_apple, or a hosted-auth provider's SDK do it correctly — but knowing the rule tells you which packages are built right, and why the "simpler" WebView-based tutorial you found is a trap.
However the token is obtained, the flow that should reach your backend is the same: the app gets an ID token from the provider, sends it to your server, and your server verifies the token cryptographically — signature against the provider's published keys, audience matching your app, expiry — before creating an account or session and issuing your own credential. Trusting an unverified token from the client — or worse, trusting an email address the app merely claims — is the classic hole, and it's indistinguishable from no auth at all to anyone who can craft an HTTP request.
The Apple rule
If your iOS app offers third-party sign-in options, Apple's guideline 4.8 requires you to also offer a privacy-focused alternative — in practice, Sign in with Apple — with equivalent prominence. Plan for it from the start rather than retrofitting it under rejection pressure the week you hoped to launch.
Two implementation notes that regularly surprise. First, users can choose to hide their real email, and you receive a relay address (@privaterelay.appleid.com) that forwards to them — your transactional email must treat it as fully valid, and your "find my account" support flows must expect users who don't know which relay address they have. Second, Apple only reliably provides the user's display name on the first authorization — capture and store it immediately during that first sign-in, or it's gone; there's no API to ask again, only an awkward "what's your name?" screen.
Where credentials live on the device
After sign-in, your backend gives the app credentials to hold, and two rules cover most of what matters:
Storage: secure storage only — Keychain on iOS, Keystore-backed encrypted storage on Android; flutter_secure_storage wraps both. Never SharedPreferences, which is a plaintext file that participates in backups and is trivially readable on a compromised device. I covered the broader credential-handling picture in Securing API Keys in Mobile Apps; user tokens are the one category that belongs on the device, precisely because they're per-user, short-lived, and revocable — a leak is a contained incident, not a compromised platform.
Lifetime: use the short-lived-access + long-lived-refresh pattern. The access token (minutes to an hour) accompanies every request; the refresh token exchanges for new access tokens and should rotate on each use — the server issues a new refresh token and invalidates the old one, which means a stolen refresh token that gets replayed collides with the legitimate one and becomes detectable, at which point you revoke the whole session. This is standard guidance for public clients like mobile apps, and most auth providers implement it if you let them.
Build silent refresh into your HTTP layer from day one: intercept the 401, refresh the token, retry the request, and only surface a login screen when the refresh itself fails. Do it in one interceptor, with concurrent-request handling (ten parallel calls hitting a 401 should trigger one refresh, not ten). Session expiry that logs users out mid-task is the most common self-inflicted auth wound in mobile apps — and it's purely a client-architecture problem, invisible in any security audit and glaring in every one-star review.
Whether the server side is stateful sessions or JWTs is its own trade-off — I've written about that in API Authentication with JWT, Explained. From the app's perspective the contract is identical either way.
The unglamorous flows are the product
Sign-in is one screen; auth is a lifecycle, and users (and attackers) judge the edges:
- Sign-out must revoke server-side, not just delete local tokens — otherwise "signed out" means "the thief has to use the API directly." And clear per-user state: caches, local database, and push tokens — a phone that keeps buzzing with a signed-out account's notifications is a privacy bug users remember.
- Password reset and email change deserve the same rigor as sign-in — they're the flows account takeovers actually go through, since "reset the password" is easier than "steal the password."
- Account deletion is required by Apple for apps with account creation, and it should be a real flow — confirm, grace period, actual data deletion — not a mailto: link and a shrug.
- Multiple providers, one person: the user who signed up with Google last year taps "Sign in with Apple" today. Decide your account-linking story early — typically verified email as the join key where both are verified, explicit account-linking UI otherwise. Retrofitting identity merging onto a live user base is genuinely painful, and duplicated accounts erode trust in ways support can't fully repair.
The takeaway
Follow the standards and the platform rules — system browser + PKCE per RFC 8252, provider tokens verified server-side before anything is trusted, Sign in with Apple offered alongside social logins, tokens in secure storage with rotation and a silent-refresh interceptor — and design the unglamorous flows (sign-out, reset, deletion, linking) like they're the product, because to a user having a bad day, they are. Done right, auth becomes boring in the best way. Done from tutorials that didn't cite the RFC, it joins the long tradition of apps whose worst reviews all start with "it logged me out again."
Further reading
- RFC 8252: OAuth 2.0 for Native Apps — the best-current-practice document this whole post leans on.
- RFC 7636: PKCE — the mechanism protecting the mobile redirect flow.
- Sign in with Apple — requirements, relay email behaviour, and setup.
- API authentication with JWT, explained — the server half of the token story.