writing

Essay

How Apps Actually Handle Your Login

I took apart how six real apps move your identity around — from broker tokens sitting in plaintext IndexedDB to four-layer encrypted envelopes — and the spread is enormous.

2026-05-20

Every app has a login. That part is universal. What is not universal — what almost nobody outside the engineering team ever sees — is what happens to your identity after you log in. Where the token lives. How it travels. Who can grab it off the wire, or out of the browser, or out of a backend database.

So I took apart six apps and watched. I captured logged-in sessions, decompiled the clients, decoded the encrypted blobs. What I found is a spectrum — from apps that hand your account away to anyone with a copy of your browser state, to apps that wrap your identity so carefully it never touches the HTTP layer at all.

The thread running through all of it: where the token lives is the whole security story. Everything else is detail.

The careless end: a token sitting in plaintext

Start with Autopilot — an Android copy-trading app[^autopilot] that lets you mirror politicians' stock portfolios. To do that, it needs to trade in your brokerage account. There is no public broker API for that, so the app does something else.

When you "connect" Robinhood, Autopilot calls a Firebase function, getBrokerageLoginScriptV2, which hands back a 30 KB chunk of JavaScript. That script is injected into a WebView pointed at Robinhood's real login page. It is a DOM state machine — a MutationObserver watches the page and a stateRecognizer() walks you through usernamePassword → selectOTPOption → enterOTP → pushNotification → liveSelfie → success. It drives the form for you, including 2FA.

Then, on the success state, the interesting part. A function called extractTokens() does not read cookies. It opens Robinhood's own IndexedDB:

window.indexedDB.open('localforage')
  .objectStore('keyvaluepairs')
  .get('reduxPersist:auth')

And lifts access_token, refresh_token, and expires_in straight out. The refresh_token is the prize — it gives Autopilot's backend permanent, headless access to api.robinhood.com. You never log in again because they never need you to.

It gets worse. The scraping requests are routed through a proxy (ProxyConfig with hostPort/username/password) using your stored client IP, so the trades look like they came from your machine — explicitly defeating the broker's new-device fraud checks. And getBrokerageLoginScriptV2 has no App Check, so anyone with curl can pull the credential-harvesting playbook for all seven supported brokers.[^appcheck]

This is the careless end. Your brokerage refresh_token ends up in a third party's database, and the architecture is designed to make that token look legitimate.

Still careless: one session unlocks everything

Autopilot is an extreme case. But you don't need a politician-copy-trading app to find the pattern. Acorns — a mainstream investing and banking app — shows a quieter version.

I captured one logged-in web session: a 104 MB HAR, 1,372 entries. The Acorns web app is almost entirely a single endpoint, POST https://graphql.acorns.com/graphql, authenticated with a plain bearer JWT in the Authorization header. GraphQL calls are often batched — the request body is an array of operations.

That batching matters. One replayed JWT, one request, and you have the entire account surface: PresentInvestmentsTotalsQuery for balances, PerformanceHoldings for every fund and share count, SpendAccountInfoQuery for the checking account and debit card, AuthProfile for the email and phone on file. From a single captured session I could reconstruct the user's UUID, verified contact info, exact balances (Invest $1,784.44, Spend $26.75), holdings down to the share, and the $100-weekly recurring deposit.

The most sensitive item: LinkedAccountDetailQuery exposes the Plaid integration — including the Plaid item id n8ee8rXzn5Hn5n1bgEk7SJ7e660avktA8EPOp and the linked Chase sub-accounts. All of it travels as plaintext JSON. Any HAR export, any browser extension, any RUM tool captures it verbatim.[^rum]

There is nothing exotic here. It is just a bearer token with no envelope around it — so the token is the account, and anything that sees the token sees everything.

The middle: encrypted, but talkative

Move up the spectrum and you get apps that protect the token well but leak in a different direction.

ChatGPT is the example I think about most. Its anti-abuse stack is genuinely serious — a Fernet-encrypted "Sentinel" fingerprint blob (POST /backend-api/sentinel/chat-requirements/prepare, body {"p":"gAAAAA..."}), a proof-of-work token from the finalize step, Cloudflare Turnstile fingerprinting on top. A replay client needs a valid Sentinel blob and PoW token, not just a cookie. That is the paranoid-end behavior.

But while I was capturing, I found this. The endpoint POST /backend-api/conversation/experimental/generate_autocompletions was hit 25 times in one session — before I had sent a single message. Each request body was {"input_text":"<partial keystroke>","num_completions":3}. Lined up, the 25 requests reconstruct exactly what I typed, including the typos ("yoi" → "yo" → "you"):

"can you search reddit and let me know more about you know the market pulse che..."

Every character I typed was POSTed to OpenAI for ghost-text autocomplete. A HAR or proxy of an unsent session reconstructs the draft you decided not to send. The token is well-protected; the typing is not. That is the middle of the spectrum — strong auth, but the design still hands away more than you'd guess.

Reddit sits near here too. Its web client mints an RS256 JWT from GET /svc/shreddit/token, 24h expiry, and the JWT payload carries an scp field — a base64 scope blob that, decoded, enumerates which internal Reddit services the token is authorized for. The auth is real, but the token quietly ships a map of the backend service mesh to every logged-in browser.[^csrf]

The paranoid end: identity that never touches HTTP

Now the other extreme — apps that treat the HTTP layer itself as hostile.

Netflix's web client has no replayable bearer token at all. Its auth is four layers stacked: HttpOnly first-party cookies (NetflixId/SecureNetflixId), a per-profile CSRF token (authURL) embedded in the HTML shell, the x-netflix.context.* routing headers — and, for anything involving playback or DRM, MSL, the Message Security Layer. MSL is Netflix's own encrypted envelope: a mastertoken, headerdata, and signature, with session keys derived from the device ESN and the Widevine/PlayReady CDM. A stolen HAR cannot drive playback, because the attacker doesn't have the CDM-bound keys.

Even Netflix's GraphQL is locked down — it's persisted-query-only. The wire carries extensions.persistedQuery.id hashes with no inline query; unknown IDs just return PersistedQueryNotFound. The one operation that can't be opaque is AleProvision (ALE = Application Layer Encryption), which returns a union of three key-exchange schemes — authenticated Diffie-Hellman with forward secrecy, RSA-OAEP key wrap, or clear (dev only) — because the client has to run WebCrypto on the result.

And then there's TeamBlind, which is the most committed of all. TeamBlind is an anonymous workplace community — the whole premise is posting about your employer without your employer knowing. So its threat model isn't a random attacker. It's your own company's network admin running a TLS-intercepting proxy.

To defeat that, TeamBlind wraps every API request and response in a hybrid-encryption envelope, on top of HTTPS:

{
  "payload":      "<SJCL JSON>",     // AES-CCM-128, iter=10000, ks=128, ts=64
  "encClientKey": "<base64, 256 B>"  // RSA-OAEP (2048-bit) wrapping the AES key
}

The payload is verbatim SJCL — Stanford's JS crypto library — output. The encClientKey is a 2048-bit RSA-wrapped per-request AES key. There is no Authorization header and no session cookie anywhere on a teamblind.com request. Identity is bound inside the encrypted payload, never at the HTTP layer. The client entry point is a single function, invokeEncryptedAction(). A corporate proxy decrypting TLS sees only opaque envelopes — it cannot attribute a post to an employee. That's not anti-scraping (you have the JS; you can call invokeEncryptedAction from your own console). It's anonymity, by construction.

What the spectrum teaches

Six apps, and the same question answered six different ways: where does the token live, and who can reach it there?

  • Autopilot puts a brokerage refresh_token in a third-party backend, disguised as your own traffic. Anyone who breaches that backend owns every connected account.
  • Acorns puts a bearer JWT on the wire in plaintext JSON. Anyone who captures one session captures the whole account.
  • ChatGPT and Reddit protect the token well — but leak adjacent things: your unsent keystrokes, a map of the internal service mesh.
  • Netflix never puts a replayable token on the wire at all; playback rides a CDM-bound encrypted envelope.
  • TeamBlind refuses to let identity touch HTTP, because it assumes the network itself is the adversary.

None of these apps is "using HTTPS wrong." They all use HTTPS. The difference is entirely in what they assume about the layer below their token — and how much of your identity they're willing to leave sitting somewhere readable. The careless apps assume the transport is safe and stop there. The paranoid ones assume nothing is safe and build their own envelope. Same login screen, completely different security story underneath.

That's the same lesson as the last post — the safety boundary has to live in a layer you actually control. The model can't be trusted to enforce its own authorization; the HTTP layer can't be trusted to protect your identity for you.

Next, I take this idea off the network entirely — into hardware, where the "wire" is a Bluetooth radio link and the token is whatever a $200 device decided to broadcast.

[^autopilot]: Autopilot (Iris Finance), Android app v1.17.6, build 6337. Decompiled with jadx (8,429 Java files) and apktool, then a live capture of the Firebase Callable Function. The Kotlin/Compose/Hilt/Apollo-GraphQL/Firebase/Room stack. [^appcheck]: The function was hittable directly: POST https://us-central1-iris-prod-autopilot.cloudfunctions.net/getBrokerageLoginScriptV2 with body {"data":{"platform":"android"}} returned automation scripts for IBKR, Schwab, Fidelity, E*TRADE, Vanguard, Yoda and Robinhood. The cookie-based brokers store session cookies in AndroidX EncryptedSharedPreferences; only Robinhood gets the IndexedDB OAuth-token path. [^rum]: The same session also fired roughly 340 third-party tracker calls — Facebook, TikTok, Snap, Datadog RUM, and more — which widens every surface that plaintext account data can leak through. [^csrf]: Reddit's CSRF design is also looser than you'd expect: a single token shared across tabs, delivered in the JSON body rather than a header, and accepted under two different casings (csrf_token and CSRF).