Краткое содержание
Я написал простое приложение C# .NET Core для аутентификации в E*Trade API с использованием OAuthv1 с целью получения котировок акций. Я могу пройти аутентификацию и получить токен запроса, перенаправить на страницу авторизации и получить строку верификатора. Однако, когда я использую строку верификатора для выполнения запроса токена доступа, примерно в 9 случаях из 10 я получаю 401 несанкционированный доступ. Но иногда это работает, и я получаю токен доступа обратно.
Подробности
Код
Я создал отдельные объекты запроса для здравомыслия, я не оставлю это так. Опять же, я могу получить токены запроса, перенаправить для авторизации и получить строку верификатора, но не токен доступа.
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}";
}
}
Например, во время написания этого поста я пару раз запускал приложение, один раз оно сработало и один раз не удалось. Я вообще код не менял.
Спасибо jdweng, 401 возвращается сразу же каждый раз. В этом случае
Мне интересно, закрывается ли старое соединение. Сервер может не разрешить второе соединение. Если вы используете from cmd.exe >netstat -a, вы можете увидеть, существует ли уже соединение. Когда код завершится, соединение должно закрыться. Когда вы потерпите неудачу, я думаю, вы увидите, что связь все еще существует.
В качестве проверки работоспособности я подключил свои параметры аутентификации к сайту, который будет генерировать подпись, просто чтобы увидеть, совпадает ли она с тем, что я получал из 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), чтобы она работала.
Мне также пришлось обновить URL-адрес строки песочницы baseEtradeApiUrl = "apisb.etrade.com";
Я столкнулся с похожей проблемой (хотя я использую 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
Я сделал две правки:
Изменен URL-адрес авторизации на то, что было размещено здесь: https://seansoper.com/blog/connecting_etrade.html
Для кнопки входа изменен поиск по идентификатору: 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.
Сколько времени нужно, чтобы вернуть 401? Если это 30 секунд, возможно, вы ищете прокси и получаете тайм-аут через 30 секунд. Возможно, вам придется отключить прокси.