docs!: Protocol 3.0 cutover — the 3.0 documentation set #73
@@ -1,126 +0,0 @@
|
||||
# Handoff — Trusted Devices & MFA: remaining work
|
||||
|
||||
> Written 2026-07-22 at the end of the backend/web implementation session, for a
|
||||
> fresh session to continue. Canonical design: `docs/website/TRUSTED_DEVICES_MFA.md`.
|
||||
> App-side plan: `docs/android/PLAN.md §4.1.1`. This doc is the "what's left + how".
|
||||
|
||||
## 1. Status at handoff
|
||||
|
||||
**Done, in review, and live-smoke-tested** (real MariaDB + AVD):
|
||||
- Backend (schema, session service, web + mobile login, self-service + admin
|
||||
endpoints, invalidation), web client UI, admin front-end UI, OpenAPI spec,
|
||||
33 server tests — all in **website PR #93** (branch `feature/trusted-devices-mfa`).
|
||||
- Docs (BACKEND_DESIGN §3/§4/§6, TRUSTED_DEVICES_MFA.md, PLAN §4.1.1) — **docs PR #32**
|
||||
(branch `docs/trusted-devices-mfa`).
|
||||
- Live smoke test passed end-to-end: trusted-device TOTP-skip (web + mobile
|
||||
`X-Trust-Token`), recovery-code login (single-use), cap 409, admin MFA reset,
|
||||
audit logging, and a real 2FA login through the Android app on an emulator.
|
||||
|
||||
**Not done — the two remaining items below.**
|
||||
|
||||
## 2. Remaining item A — merge gate (no code)
|
||||
|
||||
- **website#93** and **docs#32** need CI green + review, then merge to `main`.
|
||||
- CI (`.gitea/workflows/pr-checks.yml`) runs server tests, client build, bot install.
|
||||
- Nothing to build here; just get them reviewed/merged. The Android work should land
|
||||
**after** #93 merges so the app builds against the merged contract.
|
||||
|
||||
## 3. Remaining item B — Android app implementation (the real work)
|
||||
|
||||
The backend is fully ready and additive; the existing app is unaffected (verified).
|
||||
The app just needs to *consume* the new endpoints. Own PR in the **`android-app`**
|
||||
repo, branch `feature/trusted-devices-mfa`. Package id `com.runicgateway.app`.
|
||||
|
||||
### 3.1 Scope (from PLAN §4.1.1)
|
||||
1. **Login/TOTP screen:** on the existing `401 { totpRequired }` step, add
|
||||
- a **"Trust this device"** checkbox, and
|
||||
- a **"use a recovery code instead"** toggle (send `recoveryCode` instead of `code`).
|
||||
2. **Trust token storage:** when a login response carries `trustToken`, store it in
|
||||
**EncryptedSharedPreferences** (same secure store as the bearer tokens — never
|
||||
plain prefs/logs). On subsequent logins send it as the **`X-Trust-Token`** header
|
||||
so the server skips the TOTP prompt.
|
||||
3. **Cap handling:** a login response with `{ trustLimitReached: true, devices }`
|
||||
means show the device list and prompt the user to revoke one
|
||||
(`DELETE /auth/me/trusted-devices/:id`) then retry trusting.
|
||||
4. **Account screens:**
|
||||
- **Trusted Devices**: list (`GET /auth/me/trusted-devices`), revoke one, untrust
|
||||
all; "trust this device" (`POST /auth/me/trusted-devices` → returns `trustToken`
|
||||
for native — store it).
|
||||
- **Recovery Codes**: show the one-time batch returned by TOTP enable; remaining
|
||||
count (`GET …/recovery-codes/status`); regenerate (`POST …/recovery-codes/generate`,
|
||||
password step-up) with a show-once display + copy/share.
|
||||
5. **Invalidation:** on logout / dead-refresh sign-out / Settings→Server switch,
|
||||
**clear the stored `trustToken`** alongside the bearer tokens. (A server-side
|
||||
password change/reset or TOTP disable already revokes it.)
|
||||
6. **Tests:** JVM `:app:testDebugUnitTest` — DTO decode for the new fields, and
|
||||
repository logic (trust-token persist/clear, recoveryCode vs code branch).
|
||||
|
||||
### 3.2 Exact API contract the app consumes
|
||||
|
||||
`POST /auth/mobile/login` — body `{ username, password, code?, recoveryCode?, trustDevice?, device_name? }`, optional header `X-Trust-Token: <token>`
|
||||
- `200` → `{ accessToken, refreshToken, expiresIn, user:{id,username,role}, trustToken?, trustLimitReached?, devices? }`
|
||||
- `trustToken` present only when `trustDevice:true` was accepted (store it).
|
||||
- `trustLimitReached:true` + `devices[]` when at the cap (login still succeeded).
|
||||
- `401` → `{ totpRequired:true, message }` (missing/invalid 2nd factor) — reveal the
|
||||
code field (existing behavior); or generic `{ message }` for bad credentials.
|
||||
- A valid `X-Trust-Token` bound to the user makes a code unnecessary → straight `200`.
|
||||
|
||||
Self-service (Bearer access token):
|
||||
- `GET /auth/me/trusted-devices` → `[{ id, platform, deviceName, userAgent, createdAt, lastUsedAt, expiresAt }]`
|
||||
- `POST /auth/me/trusted-devices` body `{ deviceName? }` → `{ trusted:true, trustToken }` (native) | `409 { error:'trusted_device_limit', devices }`
|
||||
- `DELETE /auth/me/trusted-devices/:id` → `{ revoked:boolean }`
|
||||
- `DELETE /auth/me/trusted-devices` → `{ revoked:number }`
|
||||
- `GET /auth/me/account/recovery-codes/status` → `{ remaining:number }`
|
||||
- `POST /auth/me/account/recovery-codes/generate` body `{ currentPassword? }` → `{ recoveryCodes:[string] }`
|
||||
- `POST /auth/me/account/totp/enable` body `{ code }` → `{ totp_enabled:true, recoveryCodes:[string] }` (codes shown once)
|
||||
|
||||
The OpenAPI spec (`website/server/swagger/swagger-output.json`, schemas `TrustedDevice`,
|
||||
`TrustDeviceResult`, `TrustedDeviceLimit`, `RecoveryCodes`) is the source of truth.
|
||||
|
||||
### 3.3 Where it likely goes in the app
|
||||
The app already handles `totpRequired` (reveals a code field — verified live), so the
|
||||
auth surface exists. Extend the existing auth Retrofit API + repository + login
|
||||
ViewModel/screen, add a secure `trustToken` accessor to the encrypted token store,
|
||||
and add two account screens. Explore `android-app/app/src/main/java/com/runicgateway/app/`
|
||||
(auth/data/core modules) at the start — don't assume file names.
|
||||
|
||||
## 4. Environment & how-to (verified this session)
|
||||
|
||||
- **DB:** Docker container `uomm-db`, MariaDB on host port **3307**, db `uomysticmoon`,
|
||||
user `uomm` (password: `docker exec uomm-db printenv MARIADB_PASSWORD`).
|
||||
- **Run the server:** `cd website/server && node src/server.js` (uses `.env`, already
|
||||
points at 127.0.0.1:3307). It ensures schema + seeds on boot. For cap testing set
|
||||
`MAX_TRUSTED_DEVICES=2`; recovery count via `RECOVERY_CODE_COUNT`.
|
||||
- Pre-existing noise: `uo-link-socket … Unsupported state` decrypt errors are an old
|
||||
encrypted `uo_link_config` row, unrelated — ignore.
|
||||
- **Server tests:** `cd website/server && DB_HOST=127.0.0.1 DB_PORT=59999 node --test`
|
||||
(dead port by design; models stubbed). Client: `cd website/client && npm test`.
|
||||
- **TOTP codes for manual testing:** from `website/server`,
|
||||
`node -e "process.stdout.write(require('speakeasy').totp({secret:'<BASE32>',encoding:'base32'}))"`.
|
||||
- **Android build:** JDK 21; `cd android-app && ./gradlew :app:assembleDebug -Pksp.incremental=false`.
|
||||
Unit tests: `./gradlew :app:testDebugUnitTest -Pksp.incremental=false`.
|
||||
- **Emulator:** SDK at `~/AppData/Local/Android/Sdk`; AVDs `Medium_Phone_API_36.1`,
|
||||
`s22_ultra`. `adb install -r -g app/build/outputs/apk/debug/app-debug.apk`.
|
||||
- **⚠ Cleartext HTTP gotcha (dev only):** the app blocks plain HTTP to `10.0.2.2`
|
||||
(`CLEARTEXT communication … not permitted`). Cleartext to `127.0.0.1` **is** allowed,
|
||||
so for local testing run `adb reverse tcp:3000 tcp:3000` and set the app's server URL
|
||||
to `http://127.0.0.1:3000` (its default). Production uses HTTPS via the proxy — not a
|
||||
code issue. (Consider whether v1 wants a `network_security_config` dev exception; not
|
||||
required for the feature.)
|
||||
|
||||
## 5. Conventions (CLAUDE.md)
|
||||
|
||||
- Branch from up-to-date `main`; commit/push as **`wtclaude`** using the token at
|
||||
`C:\Users\colby\.gitea_token_claude` via `http.extraHeader` (never inline in URL).
|
||||
- Conventional Commits; end commit messages with the `Co-Authored-By: Claude …` +
|
||||
`Claude-Session:` trailers. PR template requires ticking the **AI-assisted** box
|
||||
(tool: Claude Code) and the license box.
|
||||
- Use `mcp__gitea__*` for PRs/issues. GPL-3.0-or-later.
|
||||
|
||||
## 6. First moves for the fresh session
|
||||
1. Check whether website#93 / docs#32 have merged (`mcp__gitea__pull_request_read`).
|
||||
2. In `android-app`: sync `main`, `git checkout -b feature/trusted-devices-mfa`.
|
||||
3. Explore the app's auth module; implement §3 against the §3.2 contract.
|
||||
4. Test with the local server + emulator via the §4 cleartext workaround.
|
||||
5. Open a single `android-app` PR; update `docs/android/PLAN.md` if the app design
|
||||
deviates from §4.1.1.
|
||||
Reference in New Issue
Block a user