From db983fbb8fdbe5c002f54fc85c291a1d80915058 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 22 Jul 2026 00:42:45 -0500 Subject: [PATCH] =?UTF-8?q?@=20docs(android):=20record=20trusted-devices?= =?UTF-8?q?=20app=20implementation=20in=20PLAN=20=C2=A74.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marks §4.1.1 implemented (app PR RunicGateway/Android-app feature/trusted-devices-mfa) and captures two deliberate design decisions from the build: - The trust token DELIBERATELY SURVIVES logout (native analogue of the rg_trust cookie): it is only consulted at a fresh login, so clearing it on logout would make the feature a no-op. Kept in a separate, username-scoped encrypted store; cleared only on Settings→Server switch, untrust-all, or server-side revocation. Supersedes the earlier handoff note. - The login-time trust cap is surfaced + resolved on the Trusted Devices screen rather than a blocking login modal, since native login already issued the session. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr @ --- android/PLAN.md | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/android/PLAN.md b/android/PLAN.md index 69c24a8..bb50044 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -544,25 +544,37 @@ Uses the existing **mobile bearer** surface, no backend changes: - `POST /auth/mobile/logout` `{ refreshToken?, all? }` (requires bearer) — revoke this session or all sessions. Called on user logout and on "sign out everywhere." -#### 4.1.1 Trusted devices & recovery codes — backend ready, app work sequenced after backend +#### 4.1.1 Trusted devices & recovery codes — **implemented** (app PR `feature/trusted-devices-mfa`) The backend trusted-device + recovery-code feature (canonical ref: `../website/TRUSTED_DEVICES_MFA.md`) -is additive on the mobile surface; the app consumes it as follows (implemented **after** the backend -lands, alongside the account screens): +is additive on the mobile surface. As built: - **Login extras:** `POST /auth/mobile/login` accepts `recoveryCode` (a single-use alternative to - `code`), `trustDevice: true`, and an `X-Trust-Token` header. On the `401 { totpRequired }` screen, - offer "use a recovery code instead" and a **"Trust this device"** checkbox. When `trustDevice` is set - and accepted, the response carries `trustToken` → **store it in EncryptedSharedPreferences** (same - store as the bearer tokens, never plain prefs/logs) and send it as `X-Trust-Token` on future logins - to skip the TOTP prompt. A `{ trustLimitReached, devices }` response means show the device list and - prompt the user to revoke one (`DELETE /auth/me/trusted-devices/:id`) then retry. -- **Self-service (account screens):** `GET /auth/me/trusted-devices`, `DELETE …/:id`, `DELETE - …/trusted-devices` (untrust all); `POST /auth/me/trusted-devices` to trust the current device from an - authenticated session (returns `{ trustToken }` for native). Recovery codes: enabling TOTP returns - the one-time `recoveryCodes` (show once, offer copy/share); `GET …/recovery-codes/status` for the - remaining count; `POST …/recovery-codes/generate` (password step-up) to regenerate. -- **Invalidation:** on logout / dead-refresh sign-out / Settings→Server switch, **clear the stored - `trustToken`** along with the bearer tokens (a password change/reset or TOTP disable already revokes - it server-side). + `code`), `trustDevice: true`, `device_name`, and an `X-Trust-Token` header. On the `401 { totpRequired }` + screen the app offers a "use a recovery code instead" toggle and a **"Trust this device"** checkbox. + When `trustDevice` is accepted, the response carries `trustToken` → stored in a **dedicated + EncryptedSharedPreferences file** (`runic_trust`, AES-256-GCM; never plain prefs/logs) **scoped to the + username** it was minted for, and replayed as `X-Trust-Token` on that account's future logins to skip + the TOTP prompt. +- **Login-time cap:** a `{ trustLimitReached, devices }` response means login **succeeded** but the + device was not remembered (session is already issued on native, unlike the web cookie step). Rather + than a blocking login-time modal, this is surfaced + resolved on the **Trusted Devices** account + screen, whose `POST /auth/me/trusted-devices` gives the exact revoke-one-then-retry flow. (Deliberate + deviation from the earlier "prompt at login" sketch — the mobile login is past the point a modal would + gate.) +- **Self-service (account screens):** a **Security** section on the account screen links to two + dedicated screens. **Trusted Devices** — `GET /auth/me/trusted-devices` (list), `DELETE …/:id` + (revoke one), `DELETE …/trusted-devices` (untrust all), and `POST /auth/me/trusted-devices` to trust + the current device (stores the returned `{ trustToken }`). **Recovery Codes** — enabling TOTP returns + the one-time `recoveryCodes` (shown once on the account screen with copy/share); `GET + …/recovery-codes/status` for the remaining count; `POST …/recovery-codes/generate` (password step-up) + to regenerate, shown once. +- **Invalidation — trust token deliberately survives logout.** The trust token is the native analogue + of the web `rg_trust` cookie, which per the canonical design *survives logout so the next login skips + 2FA*. On mobile the token is only ever consulted at a **fresh** login — i.e. exactly after a logout or + a dead-refresh sign-out — so clearing it there would make the feature a no-op. It is therefore kept in + its own store, **untouched by session teardown** (`SessionManager.onSignedOut`), and cleared only on a + **Settings → Server switch** (bound to the old host), an **untrust-all**, or server-side revocation + (password change/reset, TOTP disable — which makes any surviving token inert; the next login just + prompts for the code). This supersedes the earlier handoff note that said to clear it on logout. ### 4.2 Website-handled flows: registration, invite, forgot-password, SSO These are **not** rebuilt in the app. The app links out to the website's own pages/API and the user