Login is a state machine, not one request
The visible login call is only the middle of a longer chain. Before the first business request the app can register a device, fetch remote configuration, obtain a challenge, submit credentials, exchange a temporary code and store cookies. Replaying only the login call misses the state that made it valid, so the reproduced call fails for a reason that looks nothing like the real one.
Start by listing every state the app holds across the session: a device identifier, a client certificate, a refresh token, an access token, cookies and any proof value. Each one has a creator and a consumer, and the chain is what you need to rebuild.
Walk backward from the protected call
Begin at the request the server rejects. Take the bearer token to the code that reads it. That is usually an authorization interceptor that pulls the token from storage and sets the header. Take that storage write to the response parser that saved it. Take the parser to the request that produced the response. Repeat until every value has a source.
jadx -d out app.apk
# where the token is stored
grep -rloE 'Editor.putString|putString(".*token|SharedPreferences' out/sources | sort -u
# where it is read and attached to a request
grep -rloE 'Authorization|Bearer|addHeader' out/sources | sort -uThe stops are usually the same set: shared preferences, an encrypted storage wrapper, a cookie jar, an authorization interceptor and a refresh branch. An OAuth flow leaves the app for a browser and returns through a deep link. A custom flow may derive a proof value from a challenge, a device key or a native method. Record the stop, what it holds and who reads it.
Document the session API specification
- The exact first-run and returning-user paths.
- Cookie, access-token and refresh-token lifetimes.
- Device registration and any key material created during setup.
- What triggers a refresh and how a retry is attempted.
- Logout and session invalidation.
- Error handling for expired, revoked and malformed credentials.
Why a captured token stops working
| Symptom | Likely cause | Where to look |
|---|---|---|
| 401 on a token you just captured | The token is bound to a cookie, a device record or a proof header. | Cookie jar, device-registration call, proof generator. |
| First call works, the next fails | The refresh token is single-use or rotates. | Refresh interceptor and the refresh response. |
| A signature error, token looks fine | A timestamp or nonce is part of the proof. | Clock source, signing interceptor, nonce generator. |
| The app works, your client does not | The app made a call the client skipped. | The order of requests and any account setup call. |
What the reconstructed client must do
The client has to acquire and refresh its own session, run the same first-run and returning-user paths and handle expired, revoked and malformed credentials. A hard-coded token turns a successful demo into a broken integration, because the moment the token changes the whole flow stops. Build the refresh through the same end point the app uses, then prove the new token is accepted by making a real call.
A worked backward trace
Suppose a business call returns 401. Read the response; it does not name the fault. Open the request in the app and note the Authorization header. Search the sources for the header name and land on an interceptor. The interceptor reads from a secure storage wrapper. The wrapper is written by a response parser. The parser belongs to a token response, and that response came from a refresh call. Now you know the client must start at the refresh call, not at the business call.
Refresh is not always triggered by a 401. A response body can name an expired token, and the app may retry once silently. Read the refresh branch to see exactly what happens before the retry and copy that order. Some flows also bind the proof to the device clock, so a small clock error can break a timestamp-bearing signature even when everything else is correct.
Record the order of the whole session, not just login. A configuration call, a device registration and a refresh can each leave state that a later call needs. A client that skips a step will fail at a point that looks unrelated to the step it skipped.
Reviewed 30 August 2026 · SReverse research desk