E*Trade API часто возвращает HTTP 401 Unauthorized при получении токена доступа, но не всегда

Краткое содержание

Я написал простое приложение C# .NET Core для аутентификации в E*Trade API с использованием OAuthv1 с целью получения котировок акций. Я могу пройти аутентификацию и получить токен запроса, перенаправить на страницу авторизации и получить строку верификатора. Однако, когда я использую строку верификатора для выполнения запроса токена доступа, примерно в 9 случаях из 10 я получаю 401 несанкционированный доступ. Но иногда это работает, и я получаю токен доступа обратно.

Подробности

  • Я использую класс .NET OAuth OAuthRequest для создания запроса. строковые параметры авторизации.
  • Я использую этот API https://apisb.etrade.com/docs/api/authorization/get_access_token.html#
  • Я загрузил этот пример приложения и сравнил URL-адреса, использовали и не обнаружили серьезных расхождений, которые могли бы объяснить это поведение. https://cdn2.etrade.net/1/18122609420.0/aempros/content/dam/etrade/developer-site/en_US/document/downloads/EtradePythonClient.zip
  • Пример приложения каждый раз работает с моими кредитами, поэтому я знаю, что они работают. Существует некоторая разница в том, как код C# генерирует подпись (возможно), которая вызывает эту проблему, и она явно недетерминирована, потому что иногда мое приложение работает.
  • Я сравнил URL-адреса, используемые для аутентификации между образцом приложения и моим, и они одинаковы.

Код

Я создал отдельные объекты запроса для здравомыслия, я не оставлю это так. Опять же, я могу получить токены запроса, перенаправить для авторизации и получить строку верификатора, но не токен доступа.

    private static async Task FetchData()
    {  
        // Values
        string consumerKey = "...";
        string consumerSecret = "...";
        string requestTokenUrl = "https://api.etrade.com/oauth/request_token";
        string authorizeUrl = "https://us.etrade.com/e/t/etws/authorize";
        string accessTokenUrl = "https://api.etrade.com/oauth/access_token";
        string quoteUrl = "https://api.etrade.com/v1/market/quote/NVDA,DJI";

        // Create the request 
        var request = new OAuthRequest
        {
            Type = OAuthRequestType.RequestToken,
            ConsumerKey = consumerKey,
            ConsumerSecret = consumerSecret,
            Method = "GET",
            RequestUrl = requestTokenUrl,
            Version = "1.0",
            Realm = "etrade.com",
            CallbackUrl = "oob",
            SignatureMethod = OAuthSignatureMethod.HmacSha1
        };

        // Make call to fetch session token
        try
        {
            HttpClient client = new HttpClient();
            
            var requestTokenUrlWithQuery = $"{requestTokenUrl}?{request.GetAuthorizationQuery()}";
            var responseString = await client.GetStringAsync(requestTokenUrlWithQuery);
            var tokenParser = new TokenParser(responseString, consumerKey);

            // Call authorization API
            var authorizeUrlWithQuery = $"{authorizeUrl}?{tokenParser.GetQueryString()}";
            
            // Open browser with the above URL 
            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = authorizeUrlWithQuery,
                UseShellExecute = true
            };
            Process.Start(psi);

            // Request input of token, copied from browser
            Console.Write("Provide auth code:");
            var authCode = Console.ReadLine();
           
            // Need auth token and verifier
            var secondRequest = new OAuthRequest
            {
                Type = OAuthRequestType.AccessToken,
                ConsumerKey = consumerKey,
                ConsumerSecret = consumerSecret,
                SignatureMethod = OAuthSignatureMethod.HmacSha1,
                Method = "GET",
                Token = tokenParser.Token,
                TokenSecret = tokenParser.Secret,
                Verifier = authCode,
                RequestUrl = accessTokenUrl,
                Version = "1.0",
                Realm = "etrade.com"
            };

            // Make access token call
            var accessTokenUrlWithQuery = $"{accessTokenUrl}?{secondRequest.GetAuthorizationQuery()}";
            responseString = await client.GetStringAsync(accessTokenUrlWithQuery);

            Console.WriteLine("Access token: " + responseString);

            // Fetch quotes
            tokenParser = new TokenParser(responseString, consumerKey);
            var thirdRequest = new OAuthRequest
            {
                Type = OAuthRequestType.ProtectedResource,
                ConsumerKey = consumerKey,
                ConsumerSecret = consumerSecret,
                SignatureMethod = OAuthSignatureMethod.HmacSha1,
                Method = "GET",
                Token = tokenParser.Token,
                TokenSecret = tokenParser.Secret,
                RequestUrl = quoteUrl,
                Version = "1.0",
                Realm = "etrade.com"
            };
            
            var quoteUrlWithQueryString = $"{quoteUrl}?{thirdRequest.GetAuthorizationQuery()}";
            responseString = await client.GetStringAsync(quoteUrlWithQueryString);

            // Dump data to console 
            Console.WriteLine(responseString);
            
        }
        catch (Exception ex)
        {
            Console.WriteLine("\n"+ ex.Message);
        }
    }

    class TokenParser {
        private readonly string consumerKey;

        public TokenParser(string responseString, string consumerKey)
        {
            NameValueCollection queryStringValues = HttpUtility.ParseQueryString(responseString);
            Token = HttpUtility.UrlDecode(queryStringValues.Get("oauth_token"));
            Secret = HttpUtility.UrlDecode(queryStringValues.Get("oauth_token_secret"));
            this.consumerKey = consumerKey;
        }

        public string Token { get; set; }
        public string Secret { get; private set; }

        public string GetQueryString()
        {
            return $"key = {consumerKey}&token = {Token}";
        }
    }

Например, во время написания этого поста я пару раз запускал приложение, один раз оно сработало и один раз не удалось. Я вообще код не менял.

Сколько времени нужно, чтобы вернуть 401? Если это 30 секунд, возможно, вы ищете прокси и получаете тайм-аут через 30 секунд. Возможно, вам придется отключить прокси.

jdweng 24.12.2020 21:56

Спасибо jdweng, 401 возвращается сразу же каждый раз. В этом случае

CAS 25.12.2020 23:44

Мне интересно, закрывается ли старое соединение. Сервер может не разрешить второе соединение. Если вы используете from cmd.exe >netstat -a, вы можете увидеть, существует ли уже соединение. Когда код завершится, соединение должно закрыться. Когда вы потерпите неудачу, я думаю, вы увидите, что связь все еще существует.

jdweng 26.12.2020 00:05
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
1 066
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

В качестве проверки работоспособности я подключил свои параметры аутентификации к сайту, который будет генерировать подпись, просто чтобы увидеть, совпадает ли она с тем, что я получал из OAuthRequest. Не было. Я решил попробовать что-то другое. Я реализовал свою логику с помощью RestSharp и почти сразу заработал. Вот код.

// Values
        string consumerKey = "...";
        string consumerSecret = "...";
        string baseEtradeApiUrl = "https://api.etrade.com";
        string baseSandboxEtradeApiUrl = "https://apisb.etrade.com";
        string authorizeUrl = "https://us.etrade.com";  
        
        try
        {
            // Step 1: fetch the request token
            var client = new RestClient(baseEtradeApiUrl);
            client.Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret, "oob");
            IRestRequest request = new RestRequest("oauth/request_token");
            var response = client.Execute(request);
            Console.WriteLine("Request tokens: " + response.Content);

            // Step 1.a: parse response 
            var qs = HttpUtility.ParseQueryString(response.Content);
            var oauthRequestToken = qs["oauth_token"];
            var oauthRequestTokenSecret = qs["oauth_token_secret"];

            // Step 2: direct to authorization page
            var authorizeClient = new RestClient(authorizeUrl);
            var authorizeRequest = new RestRequest("e/t/etws/authorize");
            authorizeRequest.AddParameter("key", consumerKey);
            authorizeRequest.AddParameter("token", oauthRequestToken);
            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = authorizeClient.BuildUri(authorizeRequest).ToString(),
                UseShellExecute = true
            };
            Process.Start(psi);

            Console.Write("Provide auth code:");
            var verifier = Console.ReadLine();

            // Step 3: fetch access token
            var accessTokenRequest = new RestRequest("oauth/access_token");
            client.Authenticator = OAuth1Authenticator.ForAccessToken(consumerKey, consumerSecret, oauthRequestToken, oauthRequestTokenSecret, verifier);
            response = client.Execute(accessTokenRequest);
            Console.WriteLine("Access tokens: " + response.Content);

            // Step 3.a: parse response 
            qs = HttpUtility.ParseQueryString(response.Content);
            var oauthAccessToken = qs["oauth_token"];
            var oauthAccessTokenSecret = qs["oauth_token_secret"];

            // Step 4: fetch quote
            var sandboxClient = new RestClient(baseSandboxEtradeApiUrl);
            var quoteRequest = new RestRequest("v1/market/quote/GOOG.json");
            sandboxClient.Authenticator = OAuth1Authenticator.ForProtectedResource(consumerKey, consumerSecret, oauthAccessToken, oauthAccessTokenSecret);
            response = sandboxClient.Execute(quoteRequest);
            Console.WriteLine("Quotes: " + response.Content);

        } catch(Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

Приведенная выше логика работает. Моя единственная рабочая теория по предыдущему вопросу заключается в том, что подпись периодически становилась недействительной. Честно говоря, я не знаю первопричины, но это решение работает, так что меня это устраивает.

Спасибо. Этот код действует до 27.05.2023. ChatGPT потратил впустую 2 дня моей жизни. К вашему сведению, я использовал более старую версию restsharp (106.11.5), чтобы она работала.

tksdotnet1 27.05.2023 18:49

Мне также пришлось обновить URL-адрес строки песочницы baseEtradeApiUrl = "apisb.etrade.com";

tksdotnet1 27.05.2023 19:27

Я столкнулся с похожей проблемой (хотя я использую JavaScript).

Вызов Получить токен запроса (/request_token) будет работать, и я смогу успешно открыть страницу Авторизация приложения в веб-браузере, где пользователь сможет успешно авторизоваться и получить токен oauth_verifier. Однако, когда я пытался подписать запрос Get Access Token, я получал 401 — oauth_problem=signature_invalid.

Причина оказалась в том, что oauth_signature и другие параметры должны быть закодированы в процентах (rfc3986). В случае потока авторизации приложения нам повезло, что веб-браузер автоматически будет процентно кодировать параметры в строке URL. Однако для вызова Get Access Token веб-браузер не используется, поэтому параметры URL-адреса не кодируются в процентах.

Например, вместо oauth_signature, равного abc123=, нам нужно oauth_signature, равное abc123%3D.

Это можно исправить с помощью rfc3986-кодирования параметров в HTTP-запросах.

Причина, по которой это сработало 1 раз из 10, вероятно, заключается в том, что вам повезло, что параметры не содержали символов, которые необходимо было закодировать в rfc3986.

Это было удивительно полезно. Я использовал ваш код плюс то, что было опубликовано здесь, чтобы автоматизировать это (поскольку срок действия токенов истекает ежедневно): Автоматическая аутентификация ETrade API

Я сделал две правки:

  1. Изменен URL-адрес авторизации на то, что было размещено здесь: https://seansoper.com/blog/connecting_etrade.html

  2. Для кнопки входа изменен поиск по идентификатору: Button btnLogOn = StaticInstanceHelper.Browser.Button(Find.ById("logon_button"));

Я столкнулся с проблемами с Watin и созданием Apartmentstate. Так сделал это:

    static void Main(string[] args)
    {
        System.Threading.Thread th = new Thread(new ThreadStart(TestAuth));
        th.SetApartmentState(ApartmentState.STA);
        th.Start();
        th.Join();
    }

Затем поместите свой код в метод TestAuth.

Другие вопросы по теме