I have a valid consumer key and I'm unable to get Interactive Brokers Web API OAuth to work.
{"error":"id: 3931, error: invalid consumer","statusCode":401}
- The endpoint I'm trying to get to work: https://www.interactivebrokers.com/webtradingapi/doc.html#tag/OAuth/paths/~1oauth~1request_token/post
- OAuth v1.0a specification https://oauth.net/core/1.0a/#auth_header_authorization
Am I missing something in my implementation?
It's also on GitHub.
var httpClient = new HttpClient{ BaseAddress = new Uri("https://www.interactivebrokers.com/tradingapi/v1/")};var restClient = new IBRestClient(httpClient);var response = await restClient.RequestTokenAsync("xxxxx");Console.WriteLine($"Response: {response}");Console.ReadLine();public sealed class IBRestClient{ private readonly HttpClient _httpClient; public IBRestClient(HttpClient httpClient) { _httpClient = httpClient; } public async ValueTask<string> RequestTokenAsync(string consumerKey) { const string requestUri = "oauth/request_token"; var request = new HttpRequestMessage(HttpMethod.Post, requestUri); var baseUrl = _httpClient.BaseAddress!.AbsoluteUri; var authorizationHeader = OAuthHelper.GetAuthorizationHeader($"{baseUrl}{requestUri}", "POST", consumerKey); var authSplit = authorizationHeader.Split(''); request.Headers.Authorization = new AuthenticationHeaderValue(authSplit[0], authSplit[1]); var response = await _httpClient.SendAsync(request); return await response.Content.ReadAsStringAsync(); }}
public static class OAuthHelper{ private static readonly RandomNumberGenerator Random = RandomNumberGenerator.Create(); public static string GetAuthorizationHeader(string uri, string method, string consumerKey) { var oauthParameters = new Dictionary<string, string> { { "oauth_consumer_key", consumerKey }, { "oauth_signature_method", "RSA-SHA256" }, { "oauth_timestamp", GetTimestamp() }, { "oauth_nonce", GetNonce() }, { "oauth_callback", "oob" } }; // The request parameters are collected, sorted and concatenated into a normalized string var queryParameters = ExtractQueryParams(uri); var oauthParamString = GetOAuthParamString(queryParameters, oauthParameters); var baseUri = GetBaseUriString(uri); // Signature Base String var signatureBaseString = GetSignatureBaseString(baseUri, method, oauthParamString); var pem = File.ReadAllText("private_encryption.pem"); var signingKey = RSA.Create(); signingKey.ImportFromPem(pem); var signature = SignSignatureBaseString(signatureBaseString, Encoding.UTF8, signingKey); oauthParameters.Add("oauth_signature", signature); // Constructs and returns the Authorization header var sb = new StringBuilder(); foreach (var param in oauthParameters) { sb .Append(sb.Length == 0 ? "OAuth " : ",") .Append(param.Key) .Append("=\"") .Append(ToUriRfc3986(param.Value)) .Append('"'); } return sb.ToString(); } /// <summary> /// Parse query parameters out of the URL. /// </summary> private static Dictionary<string, List<string>> ExtractQueryParams(string uri) { var queryParamCollection = new Dictionary<string, List<string>>(); var beginIndex = uri.IndexOf('?'); if (beginIndex <= 0) { return queryParamCollection; } var rawQueryString = uri[beginIndex..]; var decodedQueryString = Uri.UnescapeDataString(rawQueryString); var mustEncode = !decodedQueryString.Equals(rawQueryString); var queryParams = rawQueryString.Split('&', '?'); foreach (var queryParam in queryParams) { if (string.IsNullOrEmpty(queryParam)) { continue; } var separatorIndex = queryParam.IndexOf('='); var key = separatorIndex < 0 ? queryParam : Uri.UnescapeDataString(queryParam[..separatorIndex]); var value = separatorIndex < 0 ? string.Empty : Uri.UnescapeDataString(queryParam[(separatorIndex + 1)..]); var encodedKey = mustEncode ? ToUriRfc3986(key) : key; var encodedValue = mustEncode ? ToUriRfc3986(value) : value; if (!queryParamCollection.ContainsKey(encodedKey)) { queryParamCollection[encodedKey] = new List<string>(); } queryParamCollection[encodedKey].Add(encodedValue); } return queryParamCollection; } /// <summary> /// Lexicographically sorts all parameters and concatenates them into a string. /// </summary> private static string GetOAuthParamString(IDictionary<string, List<string>> queryParameters, IDictionary<string, string> oauthParameters) { var sortedParameters = new SortedDictionary<string, List<string>>(queryParameters, StringComparer.Ordinal); foreach (var oauthParameter in oauthParameters) { sortedParameters[oauthParameter.Key] = new List<string> { oauthParameter.Value }; } // Build the OAuth parameter string var parameterString = new StringBuilder(); foreach (var parameter in sortedParameters) { var values = parameter.Value; values.Sort(StringComparer.Ordinal); // Keys with same name are sorted by their values foreach (var value in values) { parameterString .Append(parameterString.Length > 0 ? "&" : string.Empty) .Append(parameter.Key) .Append('=') .Append(value); } } return parameterString.ToString(); } /// <summary> /// Normalizes the URL. /// </summary> private static string GetBaseUriString(string uriString) { var uri = new Uri(uriString); var lowerCaseScheme = uri.Scheme.ToLower(); var lowerCaseAuthority = uri.Authority.ToLower(); var path = uri.AbsolutePath; if (("http".Equals(lowerCaseScheme) && uri.Port == 80) || ("https".Equals(lowerCaseScheme) && uri.Port == 443)) { // Remove port if it matches the default for scheme var index = lowerCaseAuthority.LastIndexOf(':'); if (index >= 0) { lowerCaseAuthority = lowerCaseAuthority[..index]; } } if (string.IsNullOrEmpty(path)) { path = "/"; } return $"{lowerCaseScheme}://{lowerCaseAuthority}{path}"; // Remove query and fragment } /// <summary> /// The Signature Base String is a consistent reproducible concatenation of the request elements into a single string. /// </summary> private static string GetSignatureBaseString(string baseUri, string httpMethod, string oauthParamString) { return httpMethod.ToUpper() // Uppercase HTTP method+"&" + ToUriRfc3986(baseUri) // Base URI+"&" + ToUriRfc3986(oauthParamString); // OAuth parameter string } /// <summary> /// Signs the signature base string using an RSA private key. /// </summary> private static string SignSignatureBaseString(string baseString, Encoding encoding, RSA privateKey) { var hash = Sha256Digest(baseString, encoding); var signedHashValue = privateKey.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return Convert.ToBase64String(signedHashValue); } /// <summary> /// Percent encodes entities. /// </summary> private static string ToUriRfc3986(string input) { if (string.IsNullOrEmpty(input)) { return input; } var escaped = new StringBuilder(Uri.EscapeDataString(input)); string[] uriRfc3986EscapedChars = { "!", "*", "'", "(", ")" }; foreach (var escapedChar in uriRfc3986EscapedChars) { escaped.Replace(escapedChar, UriHelper.HexEscape(escapedChar[0])); } return escaped.ToString(); } /// <summary> /// Returns a cryptographic hash of the given input. /// </summary> private static byte[] Sha256Digest(string input, Encoding encoding) { var inputBytes = encoding.GetBytes(input); return SHA256.HashData(inputBytes); } /// <summary> /// Generates a 16 char random string for replay protection. /// </summary> private static string GetNonce() { var data = new byte[8]; Random.GetBytes(data); return BitConverter.ToString(data).Replace("-", string.Empty).ToLower(); } /// <summary> /// Returns UNIX Timestamp. /// </summary> private static string GetTimestamp() { return DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); }}internal static class UriHelper{ private static readonly char[] HexUpperChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; internal static string HexEscape(char character) { if (character > '\xff') { throw new ArgumentOutOfRangeException(nameof(character)); } var chars = new char[3]; var pos = 0; EscapeAsciiChar(character, chars, ref pos); return new string(chars); } private static void EscapeAsciiChar(char ch, char[] to, ref int pos) { to[pos++] = '%'; to[pos++] = HexUpperChars[(ch & 0xf0) >> 4]; to[pos++] = HexUpperChars[ch & 0xf]; }}