OAuth: pending auth lost on process death #15

Closed
opened 2026-06-18 00:02:09 +00:00 by starsetbyte · 1 comment
Owner

Problem

Pending authorization is memory-only. Android can kill app while in browser. On return, no pending auth.

Proposed fix

Persist state, code verifier, DID, PDS URL, metadata, DPoP key before opening browser. Private key behind Android Keystore.

Source: Codex code review — auth #1

## Problem Pending authorization is memory-only. Android can kill app while in browser. On return, no pending auth. ## Proposed fix Persist state, code verifier, DID, PDS URL, metadata, DPoP key before opening browser. Private key behind Android Keystore. Source: Codex code review — auth #1
Author
Owner

Fix up in PR #32 (fix/oauth-pending-auth-process-death).

Root cause confirmed: the pending authorization (DPoP key, PKCE verifier, CSRF state, discovery) lived only in memory — on the AtProtoOAuthClient singleton and duplicated as pendingState in OAuthLoginViewModel. Process death during the browser step wiped both, so completeLogin threw No pending auth.

New PendingAuthStore persists it to EncryptedSharedPreferences before the browser opens and restores it on return; the persisted CSRF state is now the single source of truth (so the ViewModel no longer holds its own copy). Cleared on success.

One deliberate scope note re: "private key behind Android Keystore": this PR persists the DPoP EC key as Base64 PKCS8/X509 in EncryptedSharedPreferences — i.e. encrypted at rest via the Keystore master key — which matches how the live session key is already stored in OAuthSessionStore. Generating a fully hardware-backed, non-exportable key would diverge the two key paths, so I left that as a follow-up (noted in the PR) to harden both stores together.

Will auto-close on merge.

Fix up in **PR #32** (`fix/oauth-pending-auth-process-death`). Root cause confirmed: the pending authorization (DPoP key, PKCE verifier, CSRF state, discovery) lived only in memory — on the `AtProtoOAuthClient` singleton and duplicated as `pendingState` in `OAuthLoginViewModel`. Process death during the browser step wiped both, so `completeLogin` threw `No pending auth`. New `PendingAuthStore` persists it to `EncryptedSharedPreferences` before the browser opens and restores it on return; the persisted CSRF state is now the single source of truth (so the ViewModel no longer holds its own copy). Cleared on success. One deliberate scope note re: "private key behind Android Keystore": this PR persists the DPoP EC key as Base64 PKCS8/X509 in EncryptedSharedPreferences — i.e. encrypted at rest via the Keystore master key — which matches how the live session key is already stored in `OAuthSessionStore`. Generating a fully hardware-backed, non-exportable key would diverge the two key paths, so I left that as a follow-up (noted in the PR) to harden both stores together. Will auto-close on merge.
Sign in to join this conversation.
No description provided.