Summary
I have written a simple C# .NET Core application to authenticate against the E*Trade API using OAuthv1 with the intention of fetching stock quotes. I am able to authenticate and get a request token, redirect to the authorization page and obtain a verifier string. However, when I use the verifier string to perform the access token request, roughly 9 times out of 10 I get 401 unauthorized. But then occasionally it works and I get the access token back.
Details
Code
I have created separate request objects for the sake of sanity, I won't leave it this way. Again, I'm able to fetch the request tokens, redirect to authorize and get the verifier string, just not the access token.
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}";
}
}
As an example, while writing this post I ran the app a couple times and it worked once and failed once. I didn't change the code at all.
As a sanity check I plugged my auth params into a site that would generate the signature just to see if it was the same as what I was getting out of OAuthRequest. It was not. I decided to try something different. I implemented my logic using RestSharp and got it working almost immediately. Here is the code.
// 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);
}
The above logic works. My only working theory on the previous issue is that the signature was periodically invalid. To be honest I don't know root cause, but this solution works so I'm good with that.