Skip to content

Fix web login: migrate to /api/v2/auth/web/login + push approval (closes #250)#355

Open
twannooitmeer wants to merge 3 commits into
pytr-org:masterfrom
twannooitmeer:fix/auth-v2-web-login
Open

Fix web login: migrate to /api/v2/auth/web/login + push approval (closes #250)#355
twannooitmeer wants to merge 3 commits into
pytr-org:masterfrom
twannooitmeer:fix/auth-v2-web-login

Conversation

@twannooitmeer

Copy link
Copy Markdown

Summary

Trade Republic deprecated the legacy /api/v1/auth/web/login endpoint. All requests to it currently return HTTP 426 CLIENT_VERSION_OUTDATED regardless of User-Agent, which makes pytr login unusable on a fresh install (refs #250). (Verified on 2026-05-29; the cutover itself happened at some earlier point.)

This PR migrates the auth flow to /api/v2/auth/web/login, which is what app.traderepublic.com currently uses.

What changed

/api/v2/auth/web/login requires four new headers

Captured from the live web app:

x-tr-platform: web
x-tr-app-version: 15.7.0          # web track, distinct from the Android app version
x-tr-device-info: <base64(JSON)>  # browser fingerprint
x-aws-waf-token: <same value as the aws-waf-token cookie>

The x-tr-device-info payload looks like:

{
  "stableDeviceId": "<64 hex chars>",
  "model": "Apple Macintosh",
  "browser": "Chrome",
  "browserVersion": "148.0.0.0",
  "os": "Mac OS",
  "osVersion": "10.15.7",
  "timezone": "Europe/Amsterdam",
  "timezoneOffset": -120,
  "screen": "1800x1169x30",
  "preferredLanguages": ["en", "en-US"],
  "numberOfCores": 12,
  "deviceMemory": 16
}

v2 uses push-approval, not a numeric code

The v2 flow no longer issues an SMS/numeric code. Instead the user approves the login via a push notification in the Trade Republic mobile app, and the web client polls GET /api/v2/auth/web/login/processes/{processId} until the response status indicates approval (and a tr_session cookie is set).

Implementation

  • pytr/api.py
    • Module-level constants TR_WEB_APP_VERSION, TR_WEB_USER_AGENT, TR_WEB_LOGIN_PATH, overridable via PYTR_TR_APP_VERSION / PYTR_TR_USER_AGENT so future TR bumps don't require a code change:
      # If TR bumps the web app version and pytr starts returning CLIENT_VERSION_OUTDATED again:
      PYTR_TR_APP_VERSION=15.8.0 pytr login
      
      # If TR starts gating by a specific browser UA:
      PYTR_TR_USER_AGENT='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36' pytr login
    • _get_device_id() persists a stable 64-hex device id at ~/.pytr/device_id.
    • _build_device_info_header() constructs the base64-encoded JSON fingerprint.
    • _auth_headers() returns all four required headers (WAF token included when known).
    • await_web_login_approval() polls the new processes/{processId} endpoint every 2 s with a 180 s timeout. Detects approval via either an explicit status field or the presence of a tr_session cookie.
    • initiate_weblogin() now POSTs to v2 with the new headers, kicks off the poll, and returns 0 to signal the v2 flow.
    • complete_weblogin('') is a no-op (approval already happened during initiation).
  • pytr/account.py
    • When initiate_weblogin() returns 0, skip the code/SMS prompts entirely and call complete_weblogin('') to persist cookies.
    • Legacy v1 prompt path is preserved unchanged for the countdown > 0 case (dead branch in practice, but safe to keep).

Tested

Verified locally end-to-end on macOS, Python 3.12: WAF token fetch → processId → push approval → session cookie → Logged in.

Notes

  • Closes App Login Broken #250.
  • Does not yet implement opt-in registration via trustDeviceRegistrationOptions (returned by the process endpoint); that would let users skip the push step on subsequent logins. Happy to follow up in a separate PR.

Trade Republic deprecated the legacy /api/v1/auth/web/login endpoint
(all requests now return HTTP 426 CLIENT_VERSION_OUTDATED regardless of
User-Agent). The current web app uses /api/v2/auth/web/login with:

  * x-tr-platform: web
  * x-tr-app-version: 15.7.0  (web track, distinct from the Android version)
  * x-tr-device-info: base64(JSON) browser fingerprint
  * x-aws-waf-token: same value as the aws-waf-token cookie

The v2 flow also no longer issues a numeric code: the user approves the
login via a push notification in the TR mobile app. The web app polls
GET /api/v2/auth/web/login/processes/{processId} until the push is
acknowledged.

Changes:
  * pytr/api.py: add TR_WEB_APP_VERSION / TR_WEB_USER_AGENT / TR_WEB_LOGIN_PATH
    constants (overridable via env), _get_device_id() persisting a stable
    UUID at ~/.pytr/device_id, _build_device_info_header(), _auth_headers()
    helper, and await_web_login_approval() polling loop.
  * initiate_weblogin() now POSTs to v2 with the new headers, polls for
    push approval, and returns 0 to signal the v2 flow.
  * complete_weblogin('') is a no-op for v2 (approval already happened).
  * pytr/account.py: when initiate_weblogin() returns 0, skip the
    code/SMS prompts and call complete_weblogin('') directly.

Refs: pytr-org#250
@MedAzizKhayati

Copy link
Copy Markdown

Worked for me!

@RealCLanger

Copy link
Copy Markdown
Collaborator

Thanks for the contribution. As the current web login method still works, would you mind keeping the coding available as well and add an option for selecting the desired one?

@twannooitmeer

Copy link
Copy Markdown
Author

@RealCLanger i can look into this sometime soon. For me the "current" sign in method was not working as my TR account only allows for sign in confirmation through in-app notification. I was not able to get TR to text me a code (hence the reworked method).

How do you suggest we implement the choice?

@MedAzizKhayati

Copy link
Copy Markdown

How do you suggest we implement the choice?

I would suggest adding an option pytr login --v2

twannooitmeer added a commit to twannooitmeer/pytr that referenced this pull request Jun 17, 2026
Per maintainer feedback on pytr-org#355: the legacy v1 web-login flow still
works for many users, so the v2 path should be opt-in rather than the
default.

- pytr/main.py: add --v2 flag to the shared login args; plumb args.v2
  through every login() call site.
- pytr/account.py: accept v2 kwarg, pass through as use_v2_login=. The
  v1 prompt/SMS path is restored byte-for-byte; the v2 push-approval
  branch only runs when v2=True.
- pytr/api.py: gate the v2 endpoint, headers (Origin/Referer, x-tr-*,
  x-aws-waf-token, macOS Chrome UA), device-id fingerprint, and
  await_web_login_approval() poll loop behind use_v2_login. Default
  TradeRepublicApi() behavior is identical to upstream master:
  - POST /api/v1/auth/web/login with session default headers
  - resend_weblogin() POSTs to v1 with _default_headers
  - complete_weblogin(code) POSTs to v1 with no extra headers
  - initiate_weblogin() returns countdownInSeconds+1
  Constants renamed for clarity: TR_V1_LOGIN_PATH, TR_V2_LOGIN_PATH,
  TR_WEB_USER_AGENT_V2 (env-overridable as before).

Usage:
  pytr login           # v1, unchanged
  pytr login --v2      # v2 push-approval flow
Per maintainer feedback on pytr-org#355: the legacy v1 web-login flow still
works for many users, so the v2 path should be opt-in rather than the
default.

- pytr/main.py: add --v2 flag to the shared login args; plumb args.v2
  through every login() call site.
- pytr/account.py: accept v2 kwarg, pass through as use_v2_login=. The
  v1 prompt/SMS path is restored byte-for-byte; the v2 push-approval
  branch only runs when v2=True.
- pytr/api.py: gate the v2 endpoint, headers (Origin/Referer, x-tr-*,
  x-aws-waf-token, macOS Chrome UA), device-id fingerprint, and
  await_web_login_approval() poll loop behind use_v2_login. Default
  TradeRepublicApi() behavior is identical to upstream master:
  - POST /api/v1/auth/web/login with session default headers
  - resend_weblogin() POSTs to v1 with _default_headers
  - complete_weblogin(code) POSTs to v1 with no extra headers
  - initiate_weblogin() returns countdownInSeconds+1
  Constants renamed for clarity: TR_V1_LOGIN_PATH, TR_V2_LOGIN_PATH,
  TR_WEB_USER_AGENT_V2 (env-overridable as before).

Usage:
  pytr login           # v1, unchanged
  pytr login --v2      # v2 push-approval flow
@twannooitmeer twannooitmeer force-pushed the fix/auth-v2-web-login branch from afa3d21 to 0c4791b Compare June 17, 2026 16:19
@twannooitmeer

Copy link
Copy Markdown
Author

I made the requested change(s)

  • pytr login was reverted back to main and keeps the legacy flow exactly as before (same endpoint, same headers, same SMS/code prompt).
  • TradeRepublicApi default constructor keeps identical behaviour to current master
  • pytr login --v2 opts into the new push-approval flow. The flag lives on the shared parse_login_args so it works for every subcommand that also executes login (pytr portfolio --v2, pytr dl_docs --v2, etc.) for users whose accounts can't get a numeric code.
  • All the v2-specific config is gated behind use_v2_login=True on the API object, so it can't affect v1 callers.
  • I also included the --v2 flag in the readme of the repo

As I do not have a TR account that can verify the legacy/v1 flow, I did not do an end to end test for that version. Ruff check and pytest were succesful. I also did a test of the --v2 flow end-to-end. This works for me as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App Login Broken

3 participants