cddf88afb4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
#!/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 <K> \\
|
|
--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 <V>
|
|
→ 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 <code>\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()}=<paste from portal>")
|
|
print(f" IBKR_ACCESS_TOKEN_SECRET_{env.upper()}=<paste from portal>\n")
|
|
print("Also set:")
|
|
print(f" IBKR_CONSUMER_KEY_{env.upper()}=<the consumer key from step 1>")
|
|
print(" IBKR_DH_PRIME=<paste DH prime hex from portal>\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())
|