ProtonVPN import: support Proton's human-verification (CAPTCHA) flow #150

Closed
opened 2026-08-24 14:19:44 -04:00 by mysticalsoap · 3 comments
Owner

Blocks the end of #33: Proton deterministically gates POST /auth behind a CAPTCHA for any appversion it does not recognize. Probe evidence (same machine, nonexistent user, seconds apart): x-pm-appversion: Other → 422/9001 with HumanVerificationMethods: ["captcha"] (no email/sms offered); the official client's appversion string → 8002 wrong-password. Impersonating the official client is off the table, so every aqomui login will hit this gate.

The 9001 reply carries what the intended flow needs: HumanVerificationToken, ExpiresAt (~30 min), and a WebUrl (verify.proton.me) a human can solve in a normal browser. After solving, retrying the request with x-pm-human-verification-token + x-pm-human-verification-token-type: captcha headers is the documented path; proton-core's authenticate() accepts additional_headers and also has human_verif_provide_token().

Test first (cheap): whether solving the WebUrl grants IP-scoped grace, i.e. a plain retried import just passes without carrying the token. If yes, #146's current message ("solve the link in the log, import again") is already the whole flow.

If the token must ride the retry: persist the pending token root-owned under ROOTDIR keyed by provider when 9001 fires, attach it as additional_headers on the next import attempt, drop it on success/expiry. (Fancier options — interactive continuation over D-Bus, optional QtWebEngine webview like old qomui's AirVPN captcha dialog — only if the simple shape fails.)

🤖 Generated with Claude Code

Blocks the end of #33: Proton deterministically gates POST /auth behind a CAPTCHA for any appversion it does not recognize. Probe evidence (same machine, nonexistent user, seconds apart): `x-pm-appversion: Other` → 422/9001 with `HumanVerificationMethods: ["captcha"]` (no email/sms offered); the official client's appversion string → 8002 wrong-password. Impersonating the official client is off the table, so every aqomui login will hit this gate. The 9001 reply carries what the intended flow needs: `HumanVerificationToken`, `ExpiresAt` (~30 min), and a `WebUrl` (verify.proton.me) a human can solve in a normal browser. After solving, retrying the request with `x-pm-human-verification-token` + `x-pm-human-verification-token-type: captcha` headers is the documented path; proton-core's `authenticate()` accepts `additional_headers` and also has `human_verif_provide_token()`. **Test first (cheap):** whether solving the WebUrl grants IP-scoped grace, i.e. a plain retried import just passes without carrying the token. If yes, #146's current message ("solve the link in the log, import again") is already the whole flow. If the token must ride the retry: persist the pending token root-owned under ROOTDIR keyed by provider when 9001 fires, attach it as additional_headers on the next import attempt, drop it on success/expiry. (Fancier options — interactive continuation over D-Bus, optional QtWebEngine webview like old qomui's AirVPN captcha dialog — only if the simple shape fails.) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Core token flow implemented in #146 (fcd50b4) after live testing proved no IP-scoped grace exists — both a fresh account-credential attempt and an OpenVPN-credential attempt drew fresh 9001s. On demand, the token+expiry persist root-owned at ROOTDIR/-hv.json; the next import attempt sends x-pm-human-verification-token(-type) headers (names confirmed from go-proton-api — proton-core's provide_token is an empty stub) and spends the file on success. This issue stays open for what's left: confirming the solved token is actually accepted live, and UX polish (auto-opening the verification page instead of a log link, or an optional QtWebEngine view).

Core token flow implemented in #146 (fcd50b4) after live testing proved no IP-scoped grace exists — both a fresh account-credential attempt and an OpenVPN-credential attempt drew fresh 9001s. On demand, the token+expiry persist root-owned at ROOTDIR/<provider>-hv.json; the next import attempt sends x-pm-human-verification-token(-type) headers (names confirmed from go-proton-api — proton-core's provide_token is an empty stub) and spends the file on success. This issue stays open for what's left: confirming the solved token is actually accepted live, and UX polish (auto-opening the verification page instead of a log link, or an optional QtWebEngine view).
Author
Owner

UX options for the verification step, assessed. The constraint: the demand surfaces inside the root service, which cannot open a browser; the URL has to reach the gui process, and the failed-signal payload cannot carry it (& is the field separator).

  1. Dedicated D-Bus signal + auto-open (recommended): service emits e.g. human_verification_needed(url), gui wires it like the other signals, shows a dialog ('Proton needs a CAPTCHA — solve the page that just opened, then import again') and calls QDesktopServices.openUrl. Moderate wiring, no new deps, uses the user's real browser (which also helps pass the CAPTCHA).
  2. Clickable link in the log tab: logText is a QPlainTextEdit — no links without swapping widgets. Weak.
  3. Embedded QtWebEngine view (old qomui had one for AirVPN): canonical postMessage flow, heaviest, new optdepend, and an embedded browser is more likely to fail the CAPTCHA than a real one. Last resort.

Current state after #146: link in the log + failure message pointing at it, token rides the retry automatically. Option 1 is the follow-up this issue should track.

UX options for the verification step, assessed. The constraint: the demand surfaces inside the root service, which cannot open a browser; the URL has to reach the gui process, and the failed-signal payload cannot carry it (& is the field separator). 1. **Dedicated D-Bus signal + auto-open (recommended)**: service emits e.g. `human_verification_needed(url)`, gui wires it like the other signals, shows a dialog ('Proton needs a CAPTCHA — solve the page that just opened, then import again') and calls `QDesktopServices.openUrl`. Moderate wiring, no new deps, uses the user's real browser (which also helps pass the CAPTCHA). 2. **Clickable link in the log tab**: logText is a QPlainTextEdit — no links without swapping widgets. Weak. 3. **Embedded QtWebEngine view** (old qomui had one for AirVPN): canonical postMessage flow, heaviest, new optdepend, and an embedded browser is more likely to *fail* the CAPTCHA than a real one. Last resort. Current state after #146: link in the log + failure message pointing at it, token rides the retry automatically. Option 1 is the follow-up this issue should track.
Author
Owner

Everything this stayed open for is done: the solved token was confirmed accepted live at #146's merge (full solve→retry→import cycle), and the UX landed in #154 — the verification page auto-opens in the user's browser, an Information dialog with a Continue button re-fires the import with the stored credentials (fresh 2FA code prompted for when the account uses one), all live-tested including the 2FA path. Remaining Proton friction is tracked separately: #147 (session persistence — makes the CAPTCHA once-per-install), #156 (tier-mismatch feedback), #152 (WireGuard).

🤖 Generated with Claude Code

Everything this stayed open for is done: the solved token was confirmed accepted live at #146's merge (full solve→retry→import cycle), and the UX landed in #154 — the verification page auto-opens in the user's browser, an Information dialog with a Continue button re-fires the import with the stored credentials (fresh 2FA code prompted for when the account uses one), all live-tested including the 2FA path. Remaining Proton friction is tracked separately: #147 (session persistence — makes the CAPTCHA once-per-install), #156 (tier-mismatch feedback), #152 (WireGuard). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
mysticalsoap/aqomui#150
No description provided.