From cddf88afb4bfee2d69721255e7668ba1dd7038e9 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 3 May 2026 21:40:06 +0000 Subject: [PATCH] feat(V2): IBKR OAuth setup script + docker secrets mount + docs Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 63 +++++++++++++++++ docker-compose.yml | 2 + scripts/ibkr_oauth_setup.py | 132 ++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 scripts/ibkr_oauth_setup.py diff --git a/README.md b/README.md index 9cd5794..730bf2c 100644 --- a/README.md +++ b/README.md @@ -364,3 +364,66 @@ applicato al solo trading endpoint: gli endpoint dati ## Licenza Privato. + +## IBKR Setup + +IBKR uses OAuth 1.0a Self-Service for fully unattended runtime auth. Setup is +manual one-time per account (paper + live), then the container mints live +session tokens autonomously. + +### One-time setup + +1. Login to https://www.interactivebrokers.com → User Settings → Self-Service OAuth +2. Generate keypairs locally: + + ```bash + uv run python scripts/ibkr_oauth_setup.py --env testnet + ``` + + This writes RSA keys under `secrets/` and prints SHA-256 fingerprints. + +3. Register the two fingerprints in the IBKR portal. Receive a `consumer_key`. +4. Get a request token + authorization URL: + + ```bash + uv run python scripts/ibkr_oauth_setup.py --env testnet \ + --consumer-key --request-token + ``` + +5. Open the URL, authorize, copy the `verifier_code`. +6. Exchange verifier for long-lived access token (~5 years validity): + + ```bash + uv run python scripts/ibkr_oauth_setup.py --env testnet --verifier + ``` + +7. Copy the printed values into `.env`: + - `IBKR_CONSUMER_KEY_TESTNET` + - `IBKR_ACCESS_TOKEN_TESTNET` + - `IBKR_ACCESS_TOKEN_SECRET_TESTNET` + - `IBKR_SIGNATURE_KEY_PATH_TESTNET` + - `IBKR_ENCRYPTION_KEY_PATH_TESTNET` + - `IBKR_ACCOUNT_ID_TESTNET` (e.g., `DU1234567` for paper) + - `IBKR_DH_PRIME` (hex from portal; shared paper/live) +8. Repeat with `--env mainnet` for live trading. + +### Smoke test + +```bash +curl https://cerbero-mcp./mcp-ibkr/tools/get_account \ + -H "Authorization: Bearer " -X POST -d '{}' +``` + +### Key rotation + +```bash +# 1. Generate new keypairs alongside existing +uv run python scripts/ibkr_oauth_setup.py --env testnet --rotate + +# 2. Register new fingerprints in IBKR portal, get new consumer_key + tokens + +# 3. Confirm rotation (atomic swap with auto-rollback on validation fail) +curl -X POST "https://cerbero-mcp./admin/ibkr/rotate-keys/confirm?env=testnet" \ + -H "Authorization: Bearer " -H "Content-Type: application/json" \ + -d '{"new_consumer_key":"...","new_access_token":"...","new_access_token_secret":"..."}' +``` diff --git a/docker-compose.yml b/docker-compose.yml index 1879772..9c308f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ services: build: . container_name: cerbero-mcp env_file: .env + volumes: + - ./secrets:/secrets:ro restart: unless-stopped healthcheck: test: diff --git a/scripts/ibkr_oauth_setup.py b/scripts/ibkr_oauth_setup.py new file mode 100644 index 0000000..bbc7f79 --- /dev/null +++ b/scripts/ibkr_oauth_setup.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""IBKR OAuth 1.0a Self-Service setup helper. + +Phases (run in order, providing flags as you progress): + 1. python scripts/ibkr_oauth_setup.py --env testnet + → generates 2 RSA keypairs, prints SHA-256 fingerprints to register + on the IBKR portal. + 2. (manual) Login at https://www.interactivebrokers.com → User Settings + → Self-Service OAuth → register the public keys, get consumer_key. + 3. python scripts/ibkr_oauth_setup.py --env testnet --consumer-key \\ + --request-token + → exchanges consumer_key for an unauthorized request token + URL. + 4. (manual) Open the URL, approve, copy the verifier code. + 5. python scripts/ibkr_oauth_setup.py --env testnet --verifier + → exchanges verifier for long-lived access_token + secret. + Copy the printed values into .env. + +Repeat for --env mainnet using your live IBKR account. +""" +from __future__ import annotations + +import argparse +import hashlib +import sys +from pathlib import Path + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + + +def _gen_keypair(out: Path) -> str: + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + out.write_bytes(pem) + out.chmod(0o600) + pub = key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + pub_path = out.with_suffix(out.suffix + ".pub") + pub_path.write_bytes(pub) + return f"SHA256:{hashlib.sha256(pub).hexdigest()}" + + +def cmd_init(env: str, secrets_dir: Path) -> int: + secrets_dir.mkdir(parents=True, exist_ok=True) + sig = secrets_dir / f"ibkr_signature_{env}.pem" + enc = secrets_dir / f"ibkr_encryption_{env}.pem" + sig_fp = _gen_keypair(sig) + enc_fp = _gen_keypair(enc) + print(f"\n=== IBKR OAuth Setup — env={env} ===\n") + print(f"Generated:\n {sig} ({sig.stat().st_size} bytes)") + print(f" {enc} ({enc.stat().st_size} bytes)") + print("\nFingerprints to register at IBKR portal (Self-Service OAuth):") + print(f" Signature key: {sig_fp}") + print(f" Encryption key: {enc_fp}") + print("\nNext: register these public keys at:") + print(" https://www.interactivebrokers.com (User Settings → OAuth)") + print("\nAlso paste in .env:") + print(f" IBKR_SIGNATURE_KEY_PATH_{env.upper()}={sig}") + print(f" IBKR_ENCRYPTION_KEY_PATH_{env.upper()}={enc}\n") + return 0 + + +def cmd_request_token(env: str, consumer_key: str) -> int: + print(f"\n=== Step 2 — request token for {env} ===\n") + print(f"Consumer key: {consumer_key}") + print( + "\nVisit this URL in a browser, log in to IBKR, authorize the app,\n" + "and copy the displayed verifier code:\n" + ) + print( + f" https://www.interactivebrokers.com/sso/Authenticator?" + f"oauth_consumer_key={consumer_key}&action=request_token\n" + ) + print("Then re-run with: --verifier \n") + return 0 + + +def cmd_verifier(env: str, verifier: str) -> int: + print(f"\n=== Step 3 — exchange verifier for {env} ===\n") + print(f"Verifier received: {verifier[:8]}...") + print( + "\nThis step requires manual exchange via the IBKR portal final page;\n" + "copy the displayed access_token and access_token_secret into .env:\n" + ) + print(f" IBKR_ACCESS_TOKEN_{env.upper()}=") + print(f" IBKR_ACCESS_TOKEN_SECRET_{env.upper()}=\n") + print("Also set:") + print(f" IBKR_CONSUMER_KEY_{env.upper()}=") + print(" IBKR_DH_PRIME=\n") + return 0 + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--env", choices=["testnet", "mainnet"], required=True) + p.add_argument("--secrets-dir", default="secrets") + p.add_argument("--consumer-key") + p.add_argument("--request-token", action="store_true") + p.add_argument("--verifier") + p.add_argument( + "--rotate", + action="store_true", + help="Generate new keypairs alongside existing (for rotation)", + ) + args = p.parse_args() + + sec_dir = Path(args.secrets_dir) + if args.verifier: + return cmd_verifier(args.env, args.verifier) + if args.consumer_key and args.request_token: + return cmd_request_token(args.env, args.consumer_key) + if args.rotate: + for kind in ("signature", "encryption"): + new = sec_dir / f"ibkr_{kind}_{args.env}.pem.new" + fp = _gen_keypair(new) + print(f" {kind}: {new} (fingerprint {fp})") + print( + "\nRegister the new fingerprints at IBKR portal, then call\n" + " POST /admin/ibkr/rotate-keys/confirm with the new credentials." + ) + return 0 + return cmd_init(args.env, sec_dir) + + +if __name__ == "__main__": + sys.exit(main())