E*Trade API 经常 returns HTTP 401 在获取访问令牌时未经授权但并非总是如此
E*Trade API frequently returns HTTP 401 Unauthorized when fetching an access token but not always
总结
我编写了一个简单的 C# .NET Core 应用程序以使用 OAuthv1 对 E*Trade API 进行身份验证,目的是获取股票报价。我能够验证并获取请求令牌,重定向到授权页面并获取验证器字符串。但是,当我使用验证器字符串执行访问令牌请求时,大约 10 次中有 9 次我得到 401 未经授权。但有时它会起作用,我会取回访问令牌。
详情
- 我正在使用 .NET OAuth class OAuthRequest 生成查询
字符串授权参数。
- 我正在使用这个API
https://apisb.etrade.com/docs/api/authorization/get_access_token.html#
- 我已经下载了这个示例应用程序并比较了
使用过并且没有发现可以解释这一点的重大差异
行为。 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}";
}
}
例如,在编写此 post 时,我 运行 应用程序几次,一次成功,一次失败。我根本没有更改代码。
作为完整性检查,我将我的 auth 参数插入到一个站点,该站点将生成签名只是为了查看它是否与我从 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);
}
以上逻辑有效。我对上一个问题的唯一工作理论是签名定期无效。老实说,我不知道根本原因,但这个解决方案有效,所以我很满意。
我运行遇到了类似的问题(虽然我用的是JavaScript)。
Web 浏览器中的 Get Request Token call (/request_token
) call would work, and I could successfully open the Authorize Application 页面,用户可以在其中成功授权并接收 oauth_verifier
令牌。
但是,当我尝试签署 Get Access Token 请求时,我会收到 401 - oauth_problem=signature_invalid.
原来是因为 oauth_signature
和其他参数必须进行百分比编码 (rfc3986)。
在授权应用程序流程的情况下,我们很幸运,Web 浏览器将自动对 URL 栏中的参数进行百分比编码。
但是,对于获取访问令牌调用,这不涉及 Web 浏览器,因此 URL 参数未进行百分比编码。
例如,我们需要 oauth_signature
等于 abc123%3D
.
,而不是 oauth_signature
等于 abc123=
这可以通过对 HTTP 请求中的参数进行 rfc3986 编码来解决。
10 次中有 1 次成功的原因可能是您很幸运,参数不包含任何需要 rfc3986 编码的字符。
总结
我编写了一个简单的 C# .NET Core 应用程序以使用 OAuthv1 对 E*Trade API 进行身份验证,目的是获取股票报价。我能够验证并获取请求令牌,重定向到授权页面并获取验证器字符串。但是,当我使用验证器字符串执行访问令牌请求时,大约 10 次中有 9 次我得到 401 未经授权。但有时它会起作用,我会取回访问令牌。
详情
- 我正在使用 .NET OAuth class OAuthRequest 生成查询 字符串授权参数。
- 我正在使用这个API https://apisb.etrade.com/docs/api/authorization/get_access_token.html#
- 我已经下载了这个示例应用程序并比较了 使用过并且没有发现可以解释这一点的重大差异 行为。 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}";
}
}
例如,在编写此 post 时,我 运行 应用程序几次,一次成功,一次失败。我根本没有更改代码。
作为完整性检查,我将我的 auth 参数插入到一个站点,该站点将生成签名只是为了查看它是否与我从 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);
}
以上逻辑有效。我对上一个问题的唯一工作理论是签名定期无效。老实说,我不知道根本原因,但这个解决方案有效,所以我很满意。
我运行遇到了类似的问题(虽然我用的是JavaScript)。
Web 浏览器中的 Get Request Token call (/request_token
) call would work, and I could successfully open the Authorize Application 页面,用户可以在其中成功授权并接收 oauth_verifier
令牌。
但是,当我尝试签署 Get Access Token 请求时,我会收到 401 - oauth_problem=signature_invalid.
原来是因为 oauth_signature
和其他参数必须进行百分比编码 (rfc3986)。
在授权应用程序流程的情况下,我们很幸运,Web 浏览器将自动对 URL 栏中的参数进行百分比编码。
但是,对于获取访问令牌调用,这不涉及 Web 浏览器,因此 URL 参数未进行百分比编码。
例如,我们需要 oauth_signature
等于 abc123%3D
.
oauth_signature
等于 abc123=
这可以通过对 HTTP 请求中的参数进行 rfc3986 编码来解决。
10 次中有 1 次成功的原因可能是您很幸运,参数不包含任何需要 rfc3986 编码的字符。