Request Token

View as MarkdownOpen in Claude

This guide documents the implementation of the Request Token step in an OAuth 1.0a flow customized for the Interactive Brokers (IBKR) Web API. Unlike standard OAuth 1.0a implementations that typically use HMAC-SHA1 for signing, this implementation uses RSA-SHA256 asymmetric signing, which is required by IBKR’s authentication model.

This is the first of three OAuth 1.0a steps in the IBKR flow:

  1. Request Token (documented here)
  2. Authorize Token (user consent, typically out-of-band)
  3. Access Token / Live Session Token exchange
1

Prerequisites

Before implementing this flow, ensure you have:

RequirementDescription
Consumer KeyIssued by IBKR upon API application registration
RSA Private KeyUsed to sign the OAuth base string (signature_key)
RealmFor TESTCONS, use “test_realm”. For all other consumer keys, use “limited_poa”
Base URLIBKR’s API gateway hostname (baseUrl)
Dependenciespycryptodome (for SHA256, PKCS1_v1_5_Signature), requests, standard base64, urllib.parse
2

Construct the Endpoint URL

The Request Token endpoint is always accessed via POST over HTTPS.

1url = f'https://api.ibkr.com/v1/api/oauth/request_token'
3

Assemble OAuth Parameters

1oauth_params = {
2 "oauth_callback": "oob",
3 "oauth_consumer_key": consumer_key,
4 "oauth_nonce": hex(random.getrandbits(128))[2:],
5 "oauth_signature_method": "RSA-SHA256",
6 "oauth_timestamp": str(int(datetime.now().timestamp()))
7}
ParameterPurpose
oauth_callbackSet to literal string "oob" (out-of-band) — IBKR does not use redirect-based callbacks for this flow
oauth_consumer_keyYour registered application’s consumer key
oauth_nonceA unique, non-guessable value per request; generated here as a 128-bit random hex string
oauth_signature_methodMust be RSA-SHA256this deviates from the OAuth 1.0a core spec’s default of HMAC-SHA1
oauth_timestampUnix epoch time (seconds), used by the server to reject stale requests
4

Build the Signature Base String

1params_string = "&".join([f"{k}={v}" for k, v in sorted(oauth_params.items())])
2base_string = f"POST&{quote_plus(url)}&{quote(params_string)}"

The OAuth 1.0a signature base string follows the format:

HTTP_METHOD & percent_encoded(URL) & percent_encoded(sorted_parameter_string)

Implementation details:

  1. Parameters are sorted alphabetically by key.
  2. The parameter string is constructed as key=value pairs joined by & before encoding — note this is a raw concatenation, not URL-encoded key/value pairs individually.
  3. The entire parameter string is then percent-encoded using quote().
  4. The URL is percent-encoded using quote_plus().
  5. The three components are joined with literal & characters.

Note regarding the NONCE value: Standard OAuth 1.0a specifies that each key and value should be individually percent-encoded before being joined, and that reserved characters use %20-style encoding (via quote(), not quote_plus()). This implementation encodes the URL with quote_plus() (which encodes spaces as +) and the joined parameter string with quote(). Confirm this matches IBKR’s server-side expectations — inconsistent encoding is the most common source of signature validation failures. Do not deviate from this pattern without testing against IBKR’s endpoint, as it has been validated against their implementation.

5

Sign the Base String with RSA-SHA256

1encoded_base_string = base_string.encode("utf-8")
2sha256_hash = SHA256.new(data=encoded_base_string)
3bytes_pkcs115_signature = PKCS1_v1_5_Signature.new(
4 rsa_key=signature_key
5).sign(msg_hash=sha256_hash)
6b64_str_pkcs115_signature = base64.b64encode(bytes_pkcs115_signature).decode("utf-8")

Process:

  1. Encode the base string to UTF-8 bytes.
  2. Compute the SHA-256 digest.
  3. Sign the digest using PKCS#1 v1.5 padding with your RSA private key (signature_key — a Crypto.PublicKey.RSA key object).
  4. Base64-encode the raw signature bytes into a transmittable string.
6

Finalize and Encode the Signature

1oauth_params["oauth_signature"] = quote_plus(b64_str_pkcs115_signature)
2oauth_params["realm"] = realm

The Base64 signature is percent-encoded (quote_plus) before insertion into the OAuth parameter set, since it may contain characters (+, /, =) that are invalid in HTTP header values.

The realm parameter is added at this stage — it is not part of the signature base string but is included in the final Authorization header, per OAuth 1.0a convention for realm scoping.

7

Construct the Authorization Header

1oauth_header = "OAuth " + ", ".join([f'{k}="{v}"' for k, v in sorted(oauth_params.items())])
2headers = {"authorization": oauth_header}
3headers["User-Agent"] = "python/3.11"

The header follows the standard OAuth scheme format:

Authorization: OAuth key1="value1", key2="value2", ...

Parameters are sorted alphabetically (matching convention, though not strictly required at this stage since the signature was already computed). User-Agent Note: IBKR’s API gateway may enforce User-Agent validation. Update this value to reflect your actual runtime/client rather than hardcoding "python/3.11" for production deployments — pin it to your actual interpreter/environment version, or set a custom identifying string as permitted by IBKR’s integration guidelines.

8

Execute the Request and retrieve the oauth_token response

1request_request = requests.post(url=url, headers=headers)
2print(pretty_request_response(request_request))
3if request_request.status_code == 200:
4 rToken = request_request.json()["oauth_token"]

The request is sent with no request body — all authentication data lives in the Authorization header. The returned Request Token should be stored for the next two steps, though it should be discarded after retrieving the Access Token.