7

I'm writing to write a C# method to generate a authentication header for Twitter. I'm trying to search twitter through this API: https://api.twitter.com/1.1/search/tweets.json.

Here's the URL I call:

https://api.twitter.com/1.1/search/tweets.json?q=%23countryman+OR+%23johncooperworks+OR+%40mini%26since_id%3d24012619984051000%26max_id%3d250126199840518145%26result_type%3dmixed%26count%3d4

Here's my method:

private string GetTwitterAuthHeader()
{
    const string oauthConsumerKey = "";
    const string oauthConsumerSecret = "";
    const string oauthToken = "";
    const string oauthTokenSecret = "";
    const string oauthVersion = "1.0";
    const string oauthSignatureMethod = "HMAC-SHA1";

    var oauthNonce = Convert.ToBase64String(new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture)));
    var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
    var oauthTimestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString(CultureInfo.InvariantCulture);

    const string resourceUrl = "https://api.twitter.com/1.1/search/tweets.json";
    const string baseFormat = "oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}" +
                                "&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}";

    var baseString = string.Format(baseFormat,
                                oauthConsumerKey,
                                oauthNonce,
                                oauthSignatureMethod,
                                oauthTimestamp,
                                oauthToken,
                                oauthVersion
                                );

    baseString = string.Concat("GET&", Uri.EscapeDataString(resourceUrl), "&", Uri.EscapeDataString(baseString));

    var compositeKey = string.Concat(Uri.EscapeDataString(oauthConsumerSecret),
                            "&", Uri.EscapeDataString(oauthTokenSecret));

    string oauthSignature;
    using (var hasher = new HMACSHA1(Encoding.ASCII.GetBytes(compositeKey)))
    {
        oauthSignature = Convert.ToBase64String(
            hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString)));
    }

    const string headerFormat = "OAuth oauth_consumer_key=\"{0}\", " +
                                "oauth_nonce=\"{1}\", " +
                                "oauth_signature=\"{2}\", " +
                                "oauth_signature_method=\"{3}\", " +
                                "oauth_timestamp=\"{4}\", " +
                                "oauth_token=\"{5}\", " +
                                "oauth_version=\"{6}\"";

    var authHeader = string.Format(headerFormat,
                            Uri.EscapeDataString(oauthConsumerKey),
                            Uri.EscapeDataString(oauthNonce),
                            Uri.EscapeDataString(oauthSignature),
                            Uri.EscapeDataString(oauthSignatureMethod),
                            Uri.EscapeDataString(oauthTimestamp),
                            Uri.EscapeDataString(oauthToken),
                            Uri.EscapeDataString(oauthVersion)
                    );

    return authHeader;
}

The error I get is:

{
    "errors": [
        {
            "message": "Bad Authentication data",
            "code": 215
        }
    ]
}

Any pointers?

Do I need to account for the actual search query in generating the auth header? (e.g. the values I append to the search API)?

I'm finding it hard to debug.

Thanks!

EDIT:

Based on feedback, here's an update:

var resourceUrl = "https://api.twitter.com/1.1/search/tweets.json";
const string baseFormat = "oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}" +
                            "&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}&q={6}";

var baseString = string.Format(baseFormat,
                            oauthConsumerKey,
                            oauthNonce,
                            oauthSignatureMethod,
                            oauthTimestamp,
                            oauthToken,
                            oauthVersion,
                            query
                            );

baseString = string.Concat("GET&", 
    Uri.EscapeDataString(resourceUrl), "&", 
    Uri.EscapeDataString(baseString));

Having read https://dev.twitter.com/oauth/overview/creating-signatures closer, this seems to be right. Still, I get the same error.

Wade
  • 741
  • 1
  • 5
  • 18
  • Yes, request parameters need to be included in the string signed in OAuth 1. – user94559 Sep 10 '14 at 17:57
  • 1
    Here's Twitter's documentation on this (but I believe it just follows the OAuth 1.0a spec): https://dev.twitter.com/oauth/overview/creating-signatures. – user94559 Sep 10 '14 at 17:58
  • 1
    this works: https://github.com/Twitterizer/Twitterizer/tree/develop/Twitterizer2/OAuth – L.B Sep 10 '14 at 17:58
  • I've updated the code so that `var resourceUrl = string.Format("https://api.twitter.com/1.1/search/tweets.json?q={0}", HttpUtility.UrlEncode(query));` includes the parameters passed in. Still, it fails. Somewhere else? – Wade Sep 10 '14 at 18:51
  • That's not how you include the parameters. (They go in the string-to-sign after an ampersand after the base URL.) – user94559 Sep 10 '14 at 19:20
  • Thanks @smarx. I've added an update above; I think this captures it correctly, but still fails. – Wade Sep 10 '14 at 20:12

2 Answers2

4

Wrap your "query" in Uri.EscapeDataString().

Your exact code above works for me if I do that. That said, you should really be escaping all of those parameter keys and values, and I'm a bit stumped as to why the nonce is working without escaping, since it's base64 encoded. But perhaps I was just lucky in my testing and never hit a nonce with a slash in it.

user94559
  • 59,196
  • 6
  • 103
  • 103
  • 4
    This was it! Gosh, amazing how the little things block us. Thanks for the help. If anyone is interested, here's the result: https://github.com/wadewegner/TwitterOAuthRESTAPI/tree/master/src – Wade Sep 12 '14 at 19:53
  • @Wade so, 2 years later, I have a question about your TwitterOAuthRESTAPI code. In the `GetHeader(...)` function, why are you doing the `UriBuilder`? Can't you just use the `uri` variable you've passed in? – Daevin Oct 13 '16 at 20:49
  • Not Wade, but he's doing that to strip off the query string, since the query string doesn't go in the resource URL part of the string-to-sign. – user94559 Oct 14 '16 at 01:25
3

So, I wrote a class that does this. Let's see how well I can break it down. Let's start with the entire class.

    public class TwitterAuthTool : IDisposable {
    private const string BASE_AUTH_URL = "https://api.twitter.com/oauth2/token";
    private const string BASE_SEARCH_URL = "https://api.twitter.com/1.1/search/tweets.json";
    private const string BASE_INVALIDATE_URL = "https://api.twitter.com/oauth2/invalidate_token";

    private AccessToken Credentials;
    private string BearerTokenCredentials;

    public TwitterAuthTool( string p_ConsumerKey, string p_ConsumerSecret ) {
        BearerTokenCredentials =
            Convert.ToBase64String(
                Encoding.ASCII.GetBytes(
                    string.Format( "{0}:{1}",
                        Uri.EscapeUriString( p_ConsumerKey ),
                        Uri.EscapeUriString( p_ConsumerSecret ) ) ) );

        HttpWebRequest _Request = HttpWebRequest.Create( BASE_AUTH_URL ) as HttpWebRequest;
        _Request.KeepAlive = false;
        _Request.Method = "POST";
        _Request.Headers.Add( "Authorization", string.Format( "Basic {0}", BearerTokenCredentials ) );
        _Request.ContentType = "application/x-www-form-urlencoded;charset=UTF-8";

        byte[] _Content = Encoding.ASCII.GetBytes( "grant_type=client_credentials" );
        using( Stream _Stream = _Request.GetRequestStream() )
            _Stream.Write( _Content, 0, _Content.Length );

        HttpWebResponse _Response = _Request.GetResponse() as HttpWebResponse;

        DataContractJsonSerializer _AccessTokenJsonSerializer = new DataContractJsonSerializer( typeof( AccessToken ) );
        Credentials = (AccessToken)_AccessTokenJsonSerializer.ReadObject( _Response.GetResponseStream() );
    }

    public List<Tweet> GetLatest( string p_Query, int p_Count = 100 ) {
        TwitterResults _TwitterResults;
        List<Tweet> _ReturnValue = new List<Tweet>();
        DataContractJsonSerializer _JsonSerializer = new DataContractJsonSerializer( typeof( TwitterResults ) );

        HttpWebRequest _Request = WebRequest.Create( string.Format( "{0}?q={1}&result_type=recent&count={2}", BASE_SEARCH_URL, p_Query, p_Count ) ) as HttpWebRequest;
        _Request.Headers.Add( "Authorization", string.Format( "Bearer {0}", Credentials.access_token ) );
        _Request.KeepAlive = false;
        _Request.Method = "GET";

        HttpWebResponse _Response = _Request.GetResponse() as HttpWebResponse;
        _TwitterResults = (TwitterResults)_JsonSerializer.ReadObject( _Response.GetResponseStream() );
        _ReturnValue.AddRange( _TwitterResults.statuses );

        while( !string.IsNullOrWhiteSpace( _TwitterResults.search_metadata.next_results ) ) {
            _Request = WebRequest.Create( string.Format( "{0}{1}", BASE_SEARCH_URL, _TwitterResults.search_metadata.next_results ) ) as HttpWebRequest;
            _Request.Headers.Add( "Authorization", string.Format( "Bearer {0}", Credentials.access_token ) );
            _Request.KeepAlive = false;
            _Request.Method = "GET";

            _Response = _Request.GetResponse() as HttpWebResponse;
            _TwitterResults = (TwitterResults)_JsonSerializer.ReadObject( _Response.GetResponseStream() );
            _ReturnValue.AddRange( _TwitterResults.statuses );
        }

        return _ReturnValue;
    }

    public List<Tweet> GetLatestSince( string p_Query, long p_SinceId, int p_Count = 100 ) {
        TwitterResults _TwitterResults;
        List<Tweet> _ReturnValue = new List<Tweet>();
        DataContractJsonSerializer _JsonSerializer = new DataContractJsonSerializer( typeof( TwitterResults ) );

        HttpWebRequest _Request = WebRequest.Create( string.Format( "{0}?q={1}&result_type=recent&count={2}&since_id={3}", BASE_SEARCH_URL, p_Query, p_Count, p_SinceId ) ) as HttpWebRequest;
        _Request.Headers.Add( "Authorization", string.Format( "Bearer {0}", Credentials.access_token ) );
        _Request.KeepAlive = false;
        _Request.Method = "GET";

        HttpWebResponse _Response = _Request.GetResponse() as HttpWebResponse;
        _TwitterResults = (TwitterResults)_JsonSerializer.ReadObject( _Response.GetResponseStream() );
        _ReturnValue.AddRange( _TwitterResults.statuses );

        while( !string.IsNullOrWhiteSpace( _TwitterResults.search_metadata.next_results ) ) {
            _Request = WebRequest.Create( string.Format( "{0}{1}", BASE_SEARCH_URL, _TwitterResults.search_metadata.next_results ) ) as HttpWebRequest;
            _Request.Headers.Add( "Authorization", string.Format( "Bearer {0}", Credentials.access_token ) );
            _Request.KeepAlive = false;
            _Request.Method = "GET";

            _Response = _Request.GetResponse() as HttpWebResponse;
            _TwitterResults = (TwitterResults)_JsonSerializer.ReadObject( _Response.GetResponseStream() );
            _ReturnValue.AddRange( _TwitterResults.statuses );
        }

        return _ReturnValue;
    }

    public void Dispose() {
        HttpWebRequest _Request = WebRequest.Create( BASE_INVALIDATE_URL ) as HttpWebRequest;
        _Request.KeepAlive = false;
        _Request.Method = "POST";
        _Request.Headers.Add( "Authorization", string.Format( "Basic {0}", BearerTokenCredentials ) );
        _Request.ContentType = "application/x-www-form-urlencoded";

        byte[] _Content = Encoding.ASCII.GetBytes( string.Format( "access_token={0}", Credentials.access_token ) );
        using( Stream _Stream = _Request.GetRequestStream() )
            _Stream.Write( _Content, 0, _Content.Length );

        try {
            _Request.GetResponse();
        } catch {
            // The bearer token will time out if this fails.
        }
    }
}

Twitter uses something OAuth2-ish. You have to login before you do ANYTHING else. This is what the constructor of the above class does. When you do that, you get back an access token. It's a simple little JSON object that easily deserializes into the following little class.

    [DataContract]
public class AccessToken {
    [DataMember]
    public string token_type;

    [DataMember]
    public string access_token;
}

Once you have the correct credentials, you use THOSE to access things. Unfortunately, the JSON objects you get back from a query are huge. I had to create 17 different classes to deserialize the entirety of it. I'll look at putting that library up on github.

As you see in the Dispose, I don't care if the auth token fails. The credentials they give you time out after 8 hours.

Chris Gardner
  • 31
  • 1
  • 3