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

#c# #oauth #access-token #http-status-code-401 #etrade-api

#c# #oauth #токен доступа #http-status-code-401 #etrade-api

Вопрос:

Краткие сведения

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

Подробные сведения

  • Я использую .ЧИСТЫЙ класс 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}amp;token={Token}";
        }
    }
 

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

Комментарии:

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

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

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

Ответ №1:

В качестве проверки работоспособности я подключил свои параметры аутентификации к сайту, который будет генерировать подпись, просто чтобы посмотреть, совпадает ли она с тем, что я получал из 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);
        }
 

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

Ответ №2:

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

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

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

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

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

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