Search code examples
c#asp.net-coretwitteroauthtwitter-oauth

ASP NET Core Twitter OAuth Request Token Issues


Background

I have a back end application that has a Twitter app setup and I can query and pull user tweet/post data. This is great, however, right now on the front end I don't have full Twitter integration setup. What I mean by this is that on the front end the user can enter any Twitter username and I want to know for sure that the Twitter username entered actually belongs to the user. With a Twitter application key you can pull public Twitter data for any twitter account which works well for large scale data ingestion and in my case proof of concept kind of work. At the point I am now, I need to have the assumption enforced in the back end that the data being analyzed for a particular Twitter screen name is also owned by the user of the account on my web application.

The proposed Twitter Solution

Here is a bunch of reference documentation I have been trying to follow.

https://developer.twitter.com/en/docs/basics/authentication/guides/log-in-with-twitter https://developer.twitter.com/en/docs/basics/authentication/api-reference/request_token https://oauth.net/core/1.0/#anchor9 https://oauth.net/core/1.0/#auth_step1

I have been trying to follow this and I have had different permutations to the code posted below (one without the callback URL as parameters, one with etc.) but at this point, not very different. I have not had any success and it's been more than a couple of days, which is killing me.

The code

This is my attempt to follow the OAuth specification proposed above in the documentation. Note that this is ASP.NET Core 2.2 + code. Also, this is the code for just Step 1 in the Twitter guide for OAuth authentication and authorization.

public async Task<string> GetUserOAuthRequestToken()
{
    int timestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
    string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(timestamp.ToString()));

    string consumerKey = twitterConfiguration.ConsumerKey;
    string oAuthCallback = twitterConfiguration.OAuthCallback;

    string requestString =
        twitterConfiguration.EndpointUrl +
        OAuthRequestTokenRoute;

    string parameterString =
        $"oauth_callback={WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}&" +
        $"oauth_consumer_key={twitterConfiguration.ConsumerKey}&" +
        $"oauth_nonce={nonce}&" +
        $"oauth_signature_method=HMAC_SHA1&" +
        $"oauth_timestamp={timestamp}" +
        $"oauth_version=1.0";

    string signatureBaseString =
        "POST&" +
        WebUtility.UrlEncode(requestString) +
        "&" +
        WebUtility.UrlEncode(parameterString);

    string signingKey =
        twitterConfiguration.ConsumerSecret +
        "&" + twitterConfiguration.AccessTokenSecret;

    byte[] signatureBaseStringBytes = Encoding.ASCII.GetBytes(signatureBaseString);
    byte[] signingKeyBytes = Encoding.ASCII.GetBytes(signingKey);

    HMACSHA1 hmacSha1 = new HMACSHA1(signingKeyBytes);
    byte[] signature = hmacSha1.ComputeHash(signatureBaseStringBytes);

    string authenticationHeaderValue =
        $"oauth_nonce=\"{nonce}\", " +
        $"oauth_callback=\"{WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}\", " +
        $"oauth_signature_method=\"HMAC_SHA1\", " +
        $"oauth_timestamp=\"{timestamp}\", " +
        $"oauth_consumer_key=\"{twitterConfiguration.ConsumerKey}\", " +
        $"oauth_signature=\"{Convert.ToBase64String(signature)}\", " +
        $"oauth_version=\"1.0\"";

    HttpRequestMessage request = new HttpRequestMessage();
    request.Method = HttpMethod.Post;
    request.RequestUri = new Uri(
        baseUri: new Uri(twitterConfiguration.EndpointUrl),
        relativeUri: OAuthRequestTokenRoute);
    request.Content = new FormUrlEncodedContent(
        new Dictionary<string, string>() {
            { "oauth_callback", twitterConfiguration.OAuthCallback }
        });
    request.Headers.Authorization = new AuthenticationHeaderValue("OAuth",
        authenticationHeaderValue);

    HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);

    if (httpResponseMessage.IsSuccessStatusCode)
    {
        return await httpResponseMessage.Content.ReadAsStringAsync();
    }
    else
    {
        return null;
    }
}

Notes I have tried to remove the callback URL from the parameters as well and that didn't work. I have tried all sort of slightly different permutations (urlencoded my signature, added the callback URL in the query string, removed it etc), but I have lost track at this point the one's I have tried and haven't (encodings, quotes etc.).

Ignore the fact that I am not serializing the response into a model yet as the goal is to first hit a success status code!

I have an integration test setup for this method as well and I keep getting 400 Bad Request with no additional information (which makes sense), but is absolutely not helping with debugging.

[Fact]
public async Task TwitterHttpClientTests_GetOAuthRequestToken_GetsToken()
{
    var result = await twitterHttpClient.GetUserOAuthRequestToken();

    Assert.NotNull(result);
}

As an aside I had some other questions as well:

  1. Is there a way to verify a user's Twitter account without going through the OAuth flow? The reason I ask this is because getting through OAuth flow is proving to be difficult
  2. Is it safe to do the first step of the Twitter login workflow on the back end and return the response to the front end? The response would carry a sensitive token and token secret. (If I were to answer this myself I would say you have to do it this way otherwise you would have to hard code app secrets into front end configuration which is worse). I ask this because this has been on my conscious since I have started this and I'm worried a bit.
  3. Is there an OAuth helper library for C# ASP.NET Core that can make this easier?

Solution

  • After hours and hours of going through the documentation I found the answer out. Turns out I missed some small details from the guides.

    1. When making a request to oauth/request_token, when you sign the request, you don't use the access token secret (for this specific request). Also, see the "Getting Signing Key" section of the signing a request guide and read the last few paragraphs. Therefore the signing key does not have the access token secret
    2. You must UrlEncode every single key and value. You must UrlEncode the authorization header as well.

    I will post the updated code for you all here in case you need this in C#. Note that this code is not clean. You should separate OAuth functionality into some other class. This was my attempt to just get it to work.

    public async Task<string> GetUserOAuthRequestToken()
    {
        int timestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
        string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(timestamp.ToString()));
    
        string consumerKey = twitterConfiguration.ConsumerKey;
        string oAuthCallback = twitterConfiguration.OAuthCallback;
    
        string requestString =
            twitterConfiguration.EndpointUrl +
            OAuthRequestTokenRoute;
    
        string parameterString =
            $"oauth_callback={WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}&" +
            $"oauth_consumer_key={WebUtility.UrlEncode(twitterConfiguration.ConsumerKey)}&" +
            $"oauth_nonce={WebUtility.UrlEncode(nonce)}&" +
            $"oauth_signature_method={WebUtility.UrlEncode(OAuthSigningAlgorithm)}&" +
            $"oauth_timestamp={WebUtility.UrlEncode(timestamp.ToString())}&" +
            $"oauth_version={WebUtility.UrlEncode("1.0")}";
    
        string signatureBaseString =
            "POST&" +
            WebUtility.UrlEncode(requestString) +
            "&" +
            WebUtility.UrlEncode(parameterString);
    
        string signingKey =
            WebUtility.UrlEncode(twitterConfiguration.ConsumerSecret) +
            "&";
    
        byte[] signatureBaseStringBytes = Encoding.ASCII.GetBytes(signatureBaseString);
        byte[] signingKeyBytes = Encoding.ASCII.GetBytes(signingKey);
    
        HMACSHA1 hmacSha1 = new HMACSHA1(signingKeyBytes);
        byte[] signature = hmacSha1.ComputeHash(signatureBaseStringBytes);
        string base64Signature = Convert.ToBase64String(signature);
    
        string authenticationHeaderValue =
            $"oauth_nonce=\"{WebUtility.UrlEncode(nonce)}\", " +
            $"oauth_callback=\"{WebUtility.UrlEncode(twitterConfiguration.OAuthCallback)}\", " +
            $"oauth_signature_method=\"{WebUtility.UrlEncode(OAuthSigningAlgorithm)}\", " +
            $"oauth_timestamp=\"{WebUtility.UrlEncode(timestamp.ToString())}\", " +
            $"oauth_consumer_key=\"{WebUtility.UrlEncode(twitterConfiguration.ConsumerKey)}\", " +
            $"oauth_signature=\"{WebUtility.UrlEncode(base64Signature)}\", " +
            $"oauth_version=\"{WebUtility.UrlEncode("1.0")}\"";
    
        HttpRequestMessage request = new HttpRequestMessage();
        request.Method = HttpMethod.Post;
        request.RequestUri = new Uri(
            baseUri: new Uri(twitterConfiguration.EndpointUrl),
            relativeUri: OAuthRequestTokenRoute);
        request.Headers.Authorization = new AuthenticationHeaderValue("OAuth",
            authenticationHeaderValue);
    
        HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(request);
    
        if (httpResponseMessage.IsSuccessStatusCode)
        {
            string response = await httpResponseMessage.Content.ReadAsStringAsync();
            return response;
        }
        else
        {
            return null;
        }
    }