I'm attempting to use Single User OAuth for Twitter to issue a search to the Twitter API. I discovered this helpful library that let's me do this: https://gist.github.com/EelcoKoster/326aa7d1b1338806b058ddcc404622b6
(based on the original work here: https://blog.dantup.com/2016/07/simplest-csharp-code-to-post-a-tweet-using-oauth)
However, while I can send queries with this library and get results for plain text searches, it chokes when I try to query a location, such as: geocode:26.201461,-98.237987,0.25mi
After some testing it appears it is the colon : character that is causing the problem. Removing it from the query gives me results (empty since there isn't such a string in twitter, but successful); adding it back gives the error: Could not authenticate you.
I tried playing with the encoding of the parameters but am not getting anywhere.
I'm sure this can be done, as the API console appears to be using this same authorization: https://dev.twitter.com/rest/tools/console
and I can do such a search, so it has to be something wrong with my signature.
This is the relevant code (I've hard coded my query for testing):
public Task<string> Search(string search)
{
// search = Uri.EscapeDataString(search);
return SendRequest("search/tweets.json?q=geocode:26.201461,-98.237987,0.25mi", HttpMethod.GET, new Dictionary<string, string>());
}
Task<string> SendRequest(string url, HttpMethod httpMethod, Dictionary<string, string> data)
{
var fullUrl = TwitterApiBaseUrl + url;
Random rand = new Random();
// Timestamps are in seconds since 1/1/1970.
var timestamp = (int) ((DateTime.UtcNow - epochUtc).TotalSeconds);
// Add all the OAuth headers and querystring parameters, we'll need to use when constructing the hash.
var query = url.Split('?');
if (query.Count() > 1)
{
if (data == null) data = new Dictionary<string, string>();
var pairs = query[1].Split('&');
foreach (var pair in pairs)
{
var keyvalue = pair.Split('=');
data.Add(keyvalue[0], keyvalue[1]);
}
}
data.Add("oauth_consumer_key", consumerKey);
data.Add("oauth_signature_method", "HMAC-SHA1");
data.Add("oauth_timestamp", timestamp.ToString());
data.Add("oauth_nonce", rand.Next(10000000, 999999999).ToString());
data.Add("oauth_token", accessToken);
data.Add("oauth_version", "1.0");
// Generate the OAuth signature and add it to our payload.
data.Add("oauth_signature", GenerateSignature(fullUrl, data, httpMethod));
// Build the OAuth HTTP Header from the data.
string oAuthHeader = GenerateOAuthHeader(data);
switch (httpMethod)
{
case HttpMethod.GET:
return SendRequest(fullUrl, oAuthHeader, null, httpMethod);
case HttpMethod.POST:
var formData = new FormUrlEncodedContent(data.Where(kvp => !kvp.Key.StartsWith("oauth_")));
return SendRequest(fullUrl, oAuthHeader, formData, httpMethod);
default: return null;
}
}
/// <summary>
/// Generate an OAuth signature from OAuth header values.
/// </summary>
string GenerateSignature(string url, Dictionary<string, string> data, HttpMethod httpMethod)
{
var sigString = string.Join(
"&",
data
.Union(data)
.Select(kvp => string.Format("{0}={1}", WebUtility.UrlEncode(kvp.Key), WebUtility.UrlEncode(kvp.Value)))
.OrderBy(s => s)
);
string urlWithoutParameters = url.Split('?')[0];
var fullSigData = string.Format(
"{0}&{1}&{2}",
httpMethod.ToString(),
Uri.EscapeDataString(urlWithoutParameters),
Uri.EscapeDataString(sigString.ToString())
);
return Convert.ToBase64String(sigHasher.ComputeHash(new ASCIIEncoding().GetBytes(fullSigData.ToString())));
}
/// <summary>
/// Generate the raw OAuth HTML header from the values (including signature).
/// </summary>
string GenerateOAuthHeader(Dictionary<string, string> data)
{
return "OAuth " + string.Join(
",",
data
.Where(kvp => kvp.Key.StartsWith("oauth_"))
.Select(kvp => string.Format("{0}=\"{1}\"", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);
}
Can anyone see what I'm doing wrong here?
aha discovered the solution shortly after posting. the problem in my case is that the querystring is being encoded, but that apparently must exactly match the url making the request. Since i'm not encoding the url to match, I get the error.
I need to modify the code to UrlEncode the querystring values in the original URL so they match what is submitted in the signature.
here is a quick and dirty solution that allowed me to continue:
Task<string> SendRequest(string url, HttpMethod httpMethod, Dictionary<string, string> data)
{
var fullUrl = TwitterApiBaseUrl;
// ...
var query = url.Split('?');
if (query.Count() > 1)
{
fullUrl += query[0] + "?";
if (data == null) data = new Dictionary<string, string>();
var pairs = query[1].Split('&');
foreach (var pair in pairs)
{
var keyvalue = pair.Split('=');
data.Add(keyvalue[0], keyvalue[1]);
fullUrl += keyvalue[0] + "=" + WebUtility.UrlEncode(keyvalue[1]) + "&";
}
fullUrl = fullUrl.TrimEnd('&');
}
else
{
fullUrl += url;
}
// ...
}
hope this is helpful to someone else!