Auth Tokens & Idempotency Quiz
DATA › Networking
Which HTTP methods are idempotent by contract?
- GET, PUT and DELETE, but not POST
- GET and POST, but not PUT
- Only GET; every write verb is non-idempotent
- All of them: HTTP requires idempotent handlers
Answer: GET, PUT and DELETE, but not POST
PUT replaces state (same result however many times) and DELETE removes it; both are defined idempotent. POST 'creates/processes' and may repeat its effect, which is exactly why payment APIs bolt idempotency keys onto POST.
A payment request times out and the app retries it. Why is an idempotency key required for this to be safe?
- The server may have executed the first request even though the response was lost
- Timeouts corrupt the TLS session and must be re-keyed
- The retry would otherwise use a stale access token
- HTTP forbids repeating a POST within the same connection
Answer: The server may have executed the first request even though the response was lost
A timeout is ambiguous: the request may have failed OR succeeded with a lost response. With the same idempotency key, the server recognises the retry and returns the stored outcome instead of charging twice.
What is the race in this token accessor when called from many coroutines?
- Several callers see expired == true and all start their own refresh
- The expiry read is not atomic with the network call, so tokens interleave corruptly
- getToken can return null while a refresh is running
- Nothing races: suspend functions are single-threaded
Answer: Several callers see expired == true and all start their own refresh
Check-then-act without exclusion: every concurrent caller can pass the expiry check before any refresh completes, firing N refreshes with all the rotation and rate-limit fallout that brings.
Which change makes that accessor single-flight?
- Guard with a Mutex and re-check expiry inside withLock before refreshing
- Mark getToken() @Synchronized
- Make cached @Volatile so the checks see fresh state
- Launch the refresh on Dispatchers.Main so calls serialize
Answer: Guard with a Mutex and re-check expiry inside withLock before refreshing
The Mutex serialises the critical section across coroutines without blocking threads, and the inner re-check turns the queued callers into no-ops. @Synchronized doesn't work across suspension points, and volatile fixes visibility, not the check-then-act race.
An alternative single-flight shape caches a Deferred of the in-flight refresh. What property does this give?
- Every concurrent caller awaits the same refresh and gets the same result
- The refresh is retried automatically until it succeeds
- Each caller gets its own token, avoiding shared state
- The refresh runs on whichever caller has the highest priority
Answer: Every concurrent caller awaits the same refresh and gets the same result
The first caller creates the Deferred (in a scope the manager owns); everyone else finds it and awaits it. One network call, one result fanned out to all, and the field is cleared when it completes.
Why is refreshing exactly once for N callers described as making the refresh idempotent in effect?
- N concurrent requests for a valid token produce one refresh and one shared result, same as a single request
- Because the server deduplicates refresh calls by client id
- Because access tokens are idempotent by definition
- Because coroutines guarantee at-most-once execution of suspend functions
Answer: N concurrent requests for a valid token produce one refresh and one shared result, same as a single request
That is the interview's connecting thread: single-flight gives 'many invocations, the effect of one'. Whether achieved client-side (Mutex/Deferred) or server-side (idempotency keys), repeated triggers must not multiply the side effect.
The refresh call itself fails with a 401 (refresh token rejected). What is the correct handling?
- Clear the session and route to re-authentication; don't loop retrying
- Retry the refresh with exponential backoff until it succeeds
- Keep serving the expired access token until a request succeeds
- Swallow the error and return the last known token to callers
Answer: Clear the session and route to re-authentication; don't loop retrying
A rejected refresh token means the session is dead (revoked, rotated-and-reused, or expired). Retrying can never succeed and may trigger reuse detection to lock the account further. Fail cleanly: wipe state, surface logged-out, and let the user authenticate.
In OkHttp, which mechanism is designed to react to a 401 by supplying credentials for a retry?
- An Authenticator installed on the client
- An application Interceptor that rewrites the response
- A network Interceptor with a while-loop of proceed() calls
- The CookieJar, storing the token as a session cookie
Answer: An Authenticator installed on the client
Authenticator is invoked exactly when a request got a 401: refresh there (synchronously, single-flight!) and return a new request with the fresh header, or null to give up. Interceptors are the right place to ATTACH the current token, not to handle challenges.
A caller awaiting the shared refresh is cancelled (its screen closed). What should the design guarantee?
- The refresh continues in the manager's own scope and other callers still complete
- The refresh is cancelled too, and other callers restart it
- The Mutex stays held until the cancelled caller resumes
- Cancellation is blocked until the token write completes
Answer: The refresh continues in the manager's own scope and other callers still complete
Run the refresh in a scope the token manager owns (or guard the final write with NonCancellable) so one caller's lifecycle doesn't abort work others depend on. withLock releases the Mutex via finally even on cancellation, so no lock is leaked.
How does refresh-token ROTATION limit the damage of a stolen refresh token?
- Replay of the old token is detected as reuse and the whole token family is revoked
- Rotation encrypts the refresh token with a device key
- The stolen token only works on the original IP address
- Rotation shortens the access token lifetime to one request
Answer: Replay of the old token is detected as reuse and the whole token family is revoked
Every refresh invalidates the previous refresh token. If the attacker uses the stolen (now old) token, or the legitimate client does after the attacker, the server sees an invalidated token being replayed, flags compromise, and revokes all descendants.
Which storage is acceptable for a refresh token on Android?
- Keystore-backed encrypted storage (e.g. encrypted DataStore/preferences)
- Plain SharedPreferences, since the app sandbox protects it
- A field in BuildConfig set at build time
- Appended to the API base URL for convenience
Answer: Keystore-backed encrypted storage (e.g. encrypted DataStore/preferences)
The sandbox is not a boundary on rooted devices or in backups, and anything in BuildConfig/URLs leaks via the APK, logs and analytics. Encrypt at rest with a Keystore key; keep the hot copy in memory; mask Authorization headers in logging.
Token and expiry are stored as two separate vars, updated one after the other. What consistency bug can readers hit, and what is the fix?
- A reader can see a new token with the old expiry (a torn pair); store one immutable value swapped atomically
- The expiry can only drift by clock skew, which re-sync fixes
- Readers block until both writes finish, causing ANRs
- Nothing: writes to two fields are flushed together by the JVM
Answer: A reader can see a new token with the old expiry (a torn pair); store one immutable value swapped atomically
Between the two writes any reader observes a mismatched pair and may judge a fresh token expired (or worse, vice versa). Group the state into one immutable data class behind a single @Volatile reference or StateFlow so every reader sees a consistent snapshot.
What is the security benefit of idempotency keys beyond correctness?
- A replayed/duplicated request cannot multiply its side effect, blunting replay attacks and double-submits
- They encrypt the request body end to end
- They authenticate the client without a token
- They prevent the server from logging the request twice
Answer: A replayed/duplicated request cannot multiply its side effect, blunting replay attacks and double-submits
Whether the duplicate comes from a network retry, a double-tap, or an attacker replaying a captured request, the server executes the effect once and returns the recorded result. Keys must be unguessable and scoped to the authenticated user so one user's key can't collide with another's.
Which logging setup is safe in a networking stack that carries bearer tokens?
- Redact the Authorization header and log bodies only in debug builds
- Log full requests in production, but only to Crashlytics
- Log everything but rotate log files hourly
- Base64-encode the token before writing it to logs
Answer: Redact the Authorization header and log bodies only in debug builds
Tokens in logs are credentials at rest in the wrong place: they flow into crash reports, analytics and support tickets. OkHttp's HttpLoggingInterceptor supports redactHeader("Authorization"); Base64 is encoding, not protection.