Zend certified PHP/Magento developer

BuildMobile: Twitter in a Windows Phone 7 App

In the two previous posts in this series Facebook in a Windows Phone 7 App and Using Windows Live ID in a Windows Phone App you saw how to use “OAuth 2” to authenticate using Facebook and Windows Live Id. Other social networking platforms use “OAuth 1” which is much more involved as you’ll see in this post as we authenticate against Twitter. Once the user has been authenticated your application will have an Access Token which is used to access any of the Twitter APIs.

Before we jump in and start to code up our application I think it’s worth covering the basics of “OAuth 1”. With the previous posts we didn’t do this with “OAuth 2” as the process is relatively simple: create login URL, get user to authenticate and retrieve access token. With “OAuth 1” the process is much more involved and unless you have an idea of what we’re trying to do it may be hard to follow the numerous code snippets.

The process of authenticating with “OAuth 1” goes like this (full details via Twitter OAuth documentation):

  • Acquire a Request Token (and associated Request Token Secret)
  • Use the Request Token to create the login URL
  • Display web browser control and navigate to the login URL for the user to enter their credentials
  • Once the user has authenticated a Pin (known as the Verifier Pin) will be displayed that the user is supposed to cut and paste into the app.
  • Parse HTML and extract Verifier Pin
  • Acquire an Access Token using Request Token, Request Token Secret and Verifier Pin
  • Use Access Token in calls to Twitter API

Step 1: Creating a Twitter Application

Open a web browser and go to the Twitter Developer Page. In order to create an application you’ll need to sign in with a set of Twitter credentials. Again, we recommend creating a dedicated Twitter account for managing your applications.

Click on the My applications link (hover mouse over your profile to see this link), followed by the Create a new application button. You’ll be prompted to enter information about the application you’re creating (Figure 1). Remember that most of this information will be seen by the user when they’re authorizing your application to access their Twitter account.

WP7 Twitter Figure 1

Figure 1

Once you click the “Create your Twitter application” button you’ll be allocated a Consume key and Consumer secret (Figure 2).

WP7 Twitter Figure 2

Figure 2

Step 2: Creating the Shell Windows Phone App

To illustrate the process of authenticating against Twitter we’ll use a similar application to the one we used in the previous post. Again we have a WebBrowser control, a Button to trigger the authentication process and two TextBlock controls to display user information.

phone:PhoneApplicationPage x:Class="TwitterSampleApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    Grid
        StackPanel x:Name="LayoutRoot" Background="Transparent"
            Button Content="Authenticate" Click="AuthenticateClick" /
            TextBlock x:Name="UserNameText" /
            TextBlock x:Name="UserIdText" /
        /StackPanel
        phone:WebBrowser x:Name="AuthenticationBrowser" Visibility="Collapsed"
                                                Navigated="BrowserNavigated" IsScriptEnabled="True" /
    /Grid
/phone:PhoneApplicationPage

Step 3: Acquire a Request Token

In order to get an Access Token for use with the Twitter APIs you first need to get a Request Token. This will be used in the preparation of the login URL, to which you’ll send the user in order for Twitter to authenticate the user and authorize your application to access the user’s Twitter account. Acquiring a Request Token is simply a matter of making an appropriately formatted HttpWebRequest. I apologise in advance as there is quite a bit of code to do such a simple task!

We’ll start with the AuthenticateClick method which is invoked when the user clicks the Authenticate button. I’ve kept this method short so that you can see the main steps: create the HttpWebRequest (in order to acquire the Request Token), parse the response for the Request Token and Request Token Secret, create the login URL using the token and finally navigate the WebBrowser control to the login URL.

private const string OAuthConsumerKeyKey = "oauth_consumer_key";
private const string OAuthVersionKey = "oauth_version";
private const string OAuthSignatureMethodKey = "oauth_signature_method";
private const string OAuthSignatureKey = "oauth_signature";
private const string OAuthTimestampKey = "oauth_timestamp";
private const string OAuthNonceKey = "oauth_nonce";
private const string OAuthTokenKey = "oauth_token";
private const string OAuthTokenSecretKey = "oauth_token_secret";
private const string OAuthVerifierKey = "oauth_verifier";
private const string OAuthPostBodyKey = "post_body";
private const string RequestUrl = "http://api.twitter.com/oauth/request_token";
private const string AuthorizeUrl = "http://api.twitter.com/oauth/authorize";
private const string AccessUrl = "http://api.twitter.com/oauth/access_token";
private string token;
private string tokenSecret;
private string pin;
private void AuthenticateClick(object sender, RoutedEventArgs e) {
    // Create the Request
    var request = CreateRequest("POST", RequestUrl);
    request.BeginGetResponse(result = {
        try {
            var req = result.AsyncState as HttpWebRequest;
            if (req == null) throw new ArgumentNullException("result", "Request parameter is null");
            using (var resp = req.EndGetResponse(result))
            using (var strm = resp.GetResponseStream())
            using (var reader = new StreamReader(strm)) {
                var responseText = reader.ReadToEnd();
                // Parse out the request token
                ExtractTokenInfo(responseText);
                // Navigate to the authorization Url
                var loginUrl = new Uri(AuthorizeUrl + "?" + OAuthTokenKey + "=" + token);
                Dispatcher.BeginInvoke(() = AuthenticationBrowser.Navigate(loginUrl));
            }
        }
        catch {
            Dispatcher.BeginInvoke(() = MessageBox.Show("Unable to retrieve request token"));
        }
    }, request);
}

Let’s break down each of these steps a little further, starting with the CreateRequest method. The parameters are the method of web request to make, in this case “POST”, and the URL for the request. Unfortunately it’s not as easy as simply creating an HttpWebRequest and setting the ‘Method’. Before doing this we need to generate a signature (essentially a hash of the parameters being sent), which is used to set the ‘Authorization’ header.

private WebRequest CreateRequest(string httpMethod, string requestUrl) {
    var requestParameters = new Dictionarystring, string();
    var secret = "";
    if (!string.IsNullOrEmpty(token)) {
        requestParameters[OAuthTokenKey] = token;
        secret = tokenSecret;
    }
    if (!string.IsNullOrEmpty(pin)) {
        requestParameters[OAuthVerifierKey] = pin;
    }
    var url = new Uri(requestUrl);
    var normalizedUrl = requestUrl;
    if (!string.IsNullOrEmpty(url.Query)) {
        normalizedUrl = requestUrl.Replace(url.Query, "");
    }
    var signature = GenerateSignature(httpMethod, normalizedUrl, url.Query, requestParameters, secret);
    requestParameters[OAuthSignatureKey] = UrlEncode(signature);
    var request = WebRequest.CreateHttp(normalizedUrl);
    request.Method = httpMethod;
    request.Headers[HttpRequestHeader.Authorization] = GenerateAuthorizationHeader(requestParameters);
    return request;
}

Drilling into the GenerateSignature method you’ll notice it makes use of both the ConsumerKey and ConsumerSecret constants. Make sure you replace both consumer_key and consumer_secret (including angle brackets) with the ‘Consumer Key’ and ‘Consumer Secret’ issued by Twitter. At this point I’ll reiterate that you should not be hard coding these or exposing these in a production application; instead place them behind a secured web service and retrieve them as required.

The GenerateSignature method involves creating the signature base (which is effectively a concatenation of the request parameters) which is then used to compute the signature using the ‘HMACSHA1’ algorithm.

private const string OAuthVersion = "1.0";
private const string Hmacsha1SignatureType = "HMAC-SHA1";
private const string ConsumerKey = "consumer_key";
private const string ConsumerSecret = "consumer_secret";
public string GenerateSignature(string httpMethod, string normalizedUrl, string queryString,
                                                           IDictionarystring, string requestParameters, string secret = null) {
    requestParameters[OAuthConsumerKeyKey] = ConsumerKey;
    requestParameters[OAuthVersionKey] = OAuthVersion;
    requestParameters[OAuthNonceKey] = GenerateNonce();
    requestParameters[OAuthTimestampKey] = GenerateTimeStamp();
    requestParameters[OAuthSignatureMethodKey] = Hmacsha1SignatureType;
    string signatureBase = GenerateSignatureBase(httpMethod, normalizedUrl,
                                                                                         queryString, requestParameters);
    var hmacsha1 = new HMACSHA1();
    var key = string.Format("{0}{1}", UrlEncode(ConsumerSecret),
                            string.IsNullOrEmpty(secret) ? "" : UrlEncode(secret));
    hmacsha1.Key = Encoding.UTF8.GetBytes(key);
    var signature = ComputeHash(signatureBase, hmacsha1);
    return signature;
}
private static readonly Random Random = new Random();
public static string GenerateNonce()  {
    // Random number between 123456 and 9999999
    return Random.Next(123456, 9999999).ToString();
}
public static string GenerateTimeStamp() {
    var now = DateTime.UtcNow;
    TimeSpan ts = now - new DateTime(1970, 1, 1, 0, 0, 0, 0);
    return Convert.ToInt64(ts.TotalSeconds).ToString();
}
public static string GenerateSignatureBase(string httpMethod, string normalizedUrl,
                                                                              string queryString,
                                                                              IDictionarystring, string requestParameters) {
    var parameters = new ListKeyValuePairstring, string(GetQueryParameters(queryString)) {
        new KeyValuePairstring, string(OAuthVersionKey, requestParameters[OAuthVersionKey]),
        new KeyValuePairstring, string(OAuthNonceKey, requestParameters[OAuthNonceKey]),
        new KeyValuePairstring, string(OAuthTimestampKey,
                                                                     requestParameters[OAuthTimestampKey]),
        new KeyValuePairstring, string(OAuthSignatureMethodKey,
                                                                     requestParameters[OAuthSignatureMethodKey]),
        new KeyValuePairstring, string(OAuthConsumerKeyKey,
                                                                     requestParameters[OAuthConsumerKeyKey]) };
    if (requestParameters.ContainsKey(OAuthVerifierKey)) {
        parameters.Add(new KeyValuePairstring, string(OAuthVerifierKey,
                                       requestParameters[OAuthVerifierKey]));
    }
    if (requestParameters.ContainsKey(OAuthTokenKey)) {
        parameters.Add(new KeyValuePairstring, string(OAuthTokenKey,
                                       requestParameters[OAuthTokenKey]));
    }
    parameters.Sort((kvp1, kvp2) = {
                            if (kvp1.Key == kvp2.Key) {
                                return string.Compare(kvp1.Value, kvp2.Value);
                            }
                            return string.Compare(kvp1.Key, kvp2.Key);
                        });
    var parameterString = BuildParameterString(parameters);
    if (requestParameters.ContainsKey(OAuthPostBodyKey)) {
        parameterString += "" + requestParameters[OAuthPostBodyKey];
    }
    var signatureBase = new StringBuilder();
    signatureBase.AppendFormat("{0}", httpMethod);
    signatureBase.AppendFormat("{0}", UrlEncode(normalizedUrl));
    signatureBase.AppendFormat("{0}", UrlEncode(parameterString));
    return signatureBase.ToString();
}
private static IEnumerableKeyValuePairstring, string GetQueryParameters(string queryString) {
    var parameters = new ListKeyValuePairstring, string();
    if (string.IsNullOrEmpty(queryString)) return parameters;
    queryString = queryString.Trim('?');
    return (from pair in queryString.Split('')
            let bits = pair.Split('=')
            where bits.Length == 2
            select new KeyValuePairstring, string(bits[0], bits[1])).ToArray();
}
private static string BuildParameterString(IEnumerableKeyValuePairstring, string parameters) {
    var sb = new StringBuilder();
    foreach (var parameter in parameters) {
        if (sb.Length  0) sb.Append('');
        sb.AppendFormat("{0}={1}", parameter.Key, parameter.Value);
    }
    return sb.ToString();
}
/// summary
/// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986.
/// /summary
private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" };
private static readonly char[] HexUpperChars =
                         new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
public static string UrlEncode(string value) {
    // Start with RFC 2396 escaping by calling the .NET method to do the work.
    // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation).
    // If it does, the escaping we do that follows it will be a no-op since the
    // characters we search for to replace can't possibly exist in the string.
    var escaped = new StringBuilder(Uri.EscapeDataString(value));
    foreach (string t in UriRfc3986CharsToEscape) {
        escaped.Replace(t, HexEscape(t[0]));
    }
    return escaped.ToString();
}
public static string HexEscape(char character) {
    var to = new char[3];
    int pos = 0;
    EscapeAsciiChar(character, to, ref pos);
    return new string(to);
}
private static void EscapeAsciiChar(char ch, char[] to, ref int pos) {
    to[pos++] = '%';
    to[pos++] = HexUpperChars[(ch  240)  4];
    to[pos++] = HexUpperChars[ch  'x000f'];
}
private static string ComputeHash(string data, HashAlgorithm hashAlgorithm) {
    byte[] dataBuffer = Encoding.UTF8.GetBytes(data);
    byte[] hashBytes = hashAlgorithm.ComputeHash(dataBuffer);
    return Convert.ToBase64String(hashBytes);
}

Returning to the CreateRequest method the remaining step is to generate the ‘Authorization’ header. The GenerateAuthorizationHeader method concatenates the request parameters, which includes the signature we just created, with the OAuth prefix. Back in the CreateRequest method this is assigned to the ‘Authorization’ header.

public static string GenerateAuthorizationHeader(IDictionarystring, string requestParameters) {
    var paras = new StringBuilder();
    foreach (var param in requestParameters) {
        if (paras.Length  0) paras.Append(",");
        paras.Append(param.Key + "="" + param.Value + """);
    }
    return "OAuth " + paras;
}

Returning back up to the AuthorizeClick method, once the web request has returned, the Request Token and Request Token Secret can be extracted from the returned string via the ExtractTokenInfo method. The response string is a series of “key=value” pairs separated by “”, which makes extracting the pairs relatively straight forward.

private IEnumerableKeyValuePairstring, string ExtractTokenInfo(string responseText) {
    if (string.IsNullOrEmpty(responseText)) return null;
    var responsePairs = (from pair in responseText.Split('')
                            let bits = pair.Split('=')
                            where bits.Length == 2
                            select new KeyValuePairstring, string(bits[0], bits[1])).ToArray();
    token = responsePairs
                  .Where(kvp = kvp.Key == OAuthTokenKey)
                  .Select(kvp = kvp.Value).FirstOrDefault();
    tokenSecret = responsePairs
                              .Where(kvp = kvp.Key == OAuthTokenSecretKey)
                              .Select(kvp = kvp.Value).FirstOrDefault();
    return responsePairs;
}

This covers the code for acquiring the Request Token. In the AuthorizeClick method this token is then used to create the loginUrl. To get the user to login, all you have to do is to navigate the WebBrowser control to the loginUrl, which is also done in the AuthorizeClick method.

Step 4: User Logs In and Approves the Application

The user will be prompted to enter their credentials and then a screen containing a PIN (the Verifier Pin) which they’re supposedly have to cut and paste into the application (Figure 3).

WP7 Twitter Figure 3

Figure 3

Step 5: Extract the Verifier Pin

Once the user has entered their credentials the WebBrowser is navigated back to the AuthorizeUrl. Note that when you signed up for an application with Twitter you did not specify a callback URL; in this case Twitter assumes that you want the user to be redirected back to the AuthorizeUrl (see the constant defined in code earlier) with the Verifier Pin displayed to the user.

Rather than having the user copy the Pin into the app, your application can simply parse the HTML and extract the Verifier Pin. Note that in the following code we use a regular expression to locate the Pin. If Twitter chances the structure of this page you may have to change this regular expression. As such, it may be that you want to have the regular expression sitting on a server somewhere and have the application periodically retrieve it. If you need to change the regular expression you can do so on the server without having to modify and republish your application.

private void BrowserNavigated(object sender, NavigationEventArgs e){
    if (AuthenticationBrowser.Visibility == Visibility.Collapsed) {
        AuthenticationBrowser.Visibility = Visibility.Visible;
    }
    if (e.Uri.AbsoluteUri.ToLower().Replace("https://", "http://") == AuthorizeUrl) {
        var htmlString = AuthenticationBrowser.SaveToString();
        var pinFinder = new Regex(@"DIV id=oauth_pin(?pin[A-Za-z0-9_]+)/DIV", RegexOptions.IgnoreCase);
        var match = pinFinder.Match(htmlString);
        if (match.Length  0) {
            var group = match.Groups["pin"];
            if (group.Length  0) {
                pin = group.Captures[0].Value;
                if (!string.IsNullOrEmpty(pin)) {
                    RetrieveAccessToken();
                }
            }
        }
        if (string.IsNullOrEmpty(pin)){
            Dispatcher.BeginInvoke(() = MessageBox.Show("Authorization denied by user"));
        }
        // Make sure pin is reset to null
        pin = null;
        AuthenticationBrowser.Visibility = Visibility.Collapsed;
    }
}

Step 6: Extract the Access Token

In the preceding code, once the Pin has been extracted the RetrieveAccessToken method is called. This performs another HttpWebRequest to Twitter exchanging the Request Token for an Access Token. This follows the same pattern that was used previously, although if you look through the code for creating the ‘Authorization’ header you’ll see that it now includes the Verifier Pin. The response is parsed in order to retrieve the Access Token and corresponding Access Token Secret, along with Id and Name of the user that was authenticated.

public void RetrieveAccessToken() {
    var request = CreateRequest("POST", AccessUrl);
    request.BeginGetResponse( result = {
        try {
            var req = result.AsyncState as HttpWebRequest;
            if (req == null) throw new ArgumentNullException("result", "Request is null");
            using (var resp = req.EndGetResponse(result))
            using (var strm = resp.GetResponseStream())
            using (var reader = new StreamReader(strm)) {
                var responseText = reader.ReadToEnd();
                var userInfo = ExtractTokenInfo(responseText);
                Dispatcher.BeginInvoke(() = {
                                                    MessageBox.Show("Access granted");
                                                    UserIdText.Text =
                                                        userInfo.Where(kvp = kvp.Key == "user_id")
                                                                       .Select(kvp = kvp.Value).FirstOrDefault();
                                                    UserNameText.Text =
                                                        userInfo.Where(kvp = kvp.Key == "screen_name")
                                                                       .Select(kvp = kvp.Value).FirstOrDefault();
                                                });
            }
        }
        catch {
            Dispatcher.BeginInvoke(() = MessageBox.Show("Unable to retrieve Access Token"));
        }
    }, request);
}

In this post you’ve seen how to authenticate against Twitter. You can use the Access Token retrieved in this process to make requests or post updates to Twitter. More information is available via the Twitter Developer Portal.