Saturday, September 18, 2010

Building a ‘real’ Windows Phone 7 Twitter App Part 2 - oAuth

Next I’m going to cover oAuth.  This is probably the trickiest part to this app, but with some help from an OpenSource library called “Hammock,” it won’t be too bad.  I want to credit a blog post I found on the subject,  but I found it wasn’t very detailed and struggled to figure out a few things on my own,
Here’s the link: http://byatool.com/c/connect-your-web-app-to-twitter-using-hammock-csharp/comment-page-1/#comment-9955

Step 1: Add Hammock Library to our project

Update 5/18/2012: Note: Hammoc is no longer hosted on Codeplex, the best way to add it is using NuGet in Visual Studio.

Hammock is an open source REST library for .NET that simplifies calling RESTful services.  All we have to do is download the file from Codeplex and add it to our solution.  The project is located at: http://hammock.codeplex.com/ .
Download the latest binaries.  There is a link on the right side of the page:
image
I just downloaded the zip file to my desktop.  Unzip the file, then browse to find the Windows Phone 7 dll.  At the time of this post, there was only a CTP build but will still work with the RTM version of the WP7 tools.
image 
Next step is to copy “Hammock.WindowsPhone.dll” and “Hammock.WindowsPhone.pdb” into our solution.  In my case, I created a new directory called “Hammock” under my Twitt project and copied the two files over:
image
There’s one additional step you need to do since these files were downloaded off the web and Windows doesn’t trust them.
Right click on the file and select “Properties”.  Notice the “Unblock” button at the bottom right.  Click it then hit “OK”.  Do the same for the second file.
image
Next step is to add the dll to our solution.  Load your “Twitt” solution that you created in Part 1.  In the Solution Explorer locate the “References” folder, right click followed by “Add Reference”, Hit the “Browse” tab and navigate to the location you put the Hammock dll. 
image
Select it and hit “OK”.  You should now see Hammock.WindowsPhone under the Refereces folder:
image
You can try rebuilding the solution to make sure the compiler doesn’t complain.

Step 2: Setup a twitter dev application

You now need to register your app with Twitter.  Go to http://dev.twitter.com/apps .  If you don’t have an existing Twitter account create one and you should be able to register a new app:
image
You’ll get a screen like this next:
image
Give your app a name & description.  If you have a website, provide one.  I just put a fake URL in there for now.  Ensure “Application Type”’ is “Browser”.  Make “Callback URL” something that you’ll remember as we will need this in our app.  I chose http://www.bing.com.  Set Access Type to “Read & Write” so you can post tweets.  I also specified the application icon I created earlier but you can leave that as the default icon.
Click “Register Application” at the bottom and if all was successful you’ll get a new Consumer Key  and Consumer Secret.  You do need to keep these secret and if they are compromised you can regenerate new ones.  I’ll show the ones the issues my app and then change them :-)
image
That’s it!  We’re now done with Twitter.  A coding we will go…
Notice my Consumer key was: gOsH5beAiw138xyj29A5AA
and my Consumer Secret was: 8NZNamErHj8YkpLUQZQVipC1p4KOu3AYYDN3pWQ
We’re going to need these later, so keep them handy.  You can log back into Twitter at any time to get these.

 

Step 3: Setting up oAuth

We’re going to need to add a new page to our application and a button to get to this new page.  So let’s start there.
Open up the MainPage.xaml and examine the xaml a bit for the Panorama control.  Notice there are two Panorama items:
<!--Panorama item one-->
<controls:PanoramaItem Header="first item">
    <!--Double line list with text wrapping-->
    <ListBox Margin="0,0,-12,0" ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="0,0,0,17" Width="432">
                    <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                    <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</controls:PanoramaItem>
<!--Panorama item two-->
<!--Use 'Orientation="Horizontal"' to enable a panel that lays out horizontally-->
<controls:PanoramaItem Header="second item">
    <!--Double line list with image placeholder and text wrapping-->
    <ListBox Margin="0,0,-12,0" ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,17">
                    <!--Replace rectangle with image-->
                    <Rectangle Height="100" Width="100" Fill="#FFE5001b" Margin="12,0,9,0"/>
                    <StackPanel Width="311">                                   
                        <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                        <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                    </StackPanel>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</controls:PanoramaItem>

Let’s add a third item right underneath item 2 for our settings:

<!--Panorama item three-->
<controls:PanoramaItem Header="settings">
    <StackPanel>
        <Button Content="Account Settings" Click="Button_Click"/>
    </StackPanel>
</controls:PanoramaItem>
I put it in a StackPanel because we’re going to add more stuff here later.
I also added a Click event and when I typed it, it automatically created my Code behind in the MainPage.xaml.cs file that looks like this:
private void Button_Click(object sender, RoutedEventArgs e)
{
}
Add the following Code to navigate to the new page that we will create
private void Button_Click(object sender, RoutedEventArgs e)
{
    NavigationService.Navigate(new Uri("/Views/TwitterAuthPage.xaml", UriKind.Relative));
}
Now we should go create that page under the “Views” folder.  Right Click on it and select “Add” –> “New Item”.  Select “Windows Phone Portrait Page” and give it the same name we have above “TwitterAuthPage.xaml” then hit the “Add” button.
 image
Run the app now and you should now have a third Panorama item:
image
Select the new “Account Settings” button and you should see our new page:
image
Next ,we will add the Web Browser Control to the page.  The way oAuth is going to work is our app will pass our client key’s to Twitter and Twitter will return a web page where the user will be prompted for their user name, password and to accept granting access to this app.  This way the user never enters in credentials into the app, where a malicious app developer could hijack those credentials or the app could get compromised.  Once Twitter is successful they will redirect to a web page of ours with a token in the query string parameter.  (Remember we used http://www.bing.com for our callback).  We then need to extract this token and make a final call to Twitter to get the set of client key’s which our app will then store and use for all authorization calls to Twitter.  That made perfect sense right?  OK, well what about that return forward to bing.com?  This is the trick I had to figure out.  We need to trap the navigation event to www.bing.com , extract the token, then cancel it and continue on in our process.
Hopefully you can follow better in code.  I’m going to dump a bunch of code here and won’t explain every detail of the Hammock call but it should be easy to figure out.
First we need to add the browser control to our new xaml page we created.  I’m also going to add a progressbar and default it to “Collapsed”.
Here’s what the changes look like in TwitterAuthPage.xaml.  Notice I’ve added two event handlers one for Navigated and another for Navigating:
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <ProgressBar x:Name="ProgressBar" VerticalAlignment="Top" IsIndeterminate="False" Visibility="Collapsed"/>
    <!--TitlePanel contains the name of the application and page title-->
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock x:Name="ApplicationTitle" Text="MY APPLICATION" Style="{StaticResource PhoneTextNormalStyle}"/>
        <TextBlock x:Name="PageTitle" Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>
    <!--ContentPanel - place additional content here-->
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
        <phone:WebBrowser x:Name="BrowserControl" Navigated="BrowserControl_Navigated" Navigating="BrowserControl_Navigating" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
    </Grid>
</Grid>
We’re going to need a helper class for some settings.  Under the project add a new folder called “Common,”  right click that and “Add” a new “Class” called TwitterSettings.cs
image
image
Make this class look like this.  Don’t forget to substitute your own Consumer Key and Secret:
namespace Twitt.Common
{
    public class TwitterSettings
    {
        public static string ConsumerKey = "gOsH5beAiw138xyj29A5AA";
        public static string ConsumerKeySecret = "8NZNamErHj8YkpLUQZQVipC1p4KOu3AYYDN3pWQ";
        public static string RequestTokenUri = "https://api.twitter.com/oauth/request_token";
        public static string OAuthVersion = "1.0";
        public static string CallbackUri = "http://www.bing.com";
        public static string AuthorizeUri = "https://api.twitter.com/oauth/authorize";
        public static string AccessTokenUri = "https://api.twitter.com/oauth/access_token";
    }
    public class TwitterAccess
    {
        public string AccessToken { get; set; }
        public string AccessTokenSecret { get; set; }
        public string UserId { get; set; }
        public string ScreenName { get; set; }
    }
}
Now in our TwitterAuthPage.xaml.cs file copy all of this code:
using System;
using System.Windows;
using System.Windows.Navigation;
using Hammock;
using Hammock.Authentication.OAuth;
using Microsoft.Phone.Controls;
using Twitt.Common;
namespace Twitt.Views
{
    public partial class TwitterAuthPage
    {
        private string _oAuthTokenSecret;
        private string _oAuthToken;
        public TwitterAuthPage()
        {
            InitializeComponent();
        }
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            GetTwitterToken();
            ProgressBar.Visibility = Visibility.Visible;
            ProgressBar.IsIndeterminate = true;
        }
        private void GetTwitterToken()
        {
            var credentials = new OAuthCredentials
            {
                Type = OAuthType.RequestToken,
                SignatureMethod = OAuthSignatureMethod.HmacSha1,
                ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
                ConsumerKey = TwitterSettings.ConsumerKey,
                ConsumerSecret = TwitterSettings.ConsumerKeySecret,
                Version = TwitterSettings.OAuthVersion,
                CallbackUrl = TwitterSettings.CallbackUri
            };
            var client = new RestClient
            {
                Authority = "https://api.twitter.com/oauth",
                Credentials = credentials,
                HasElevatedPermissions = true,
                          SilverlightAcceptEncodingHeader = "gizp",
                          DecompressionMethods = DecompressionMethods.GZip,
             }; 
            var request = new RestRequest
            {
                Path = "/request_token"
            };
            client.BeginRequest(request, new RestCallback(TwitterRequestTokenCompleted));
        }
        private void TwitterRequestTokenCompleted(RestRequest request, RestResponse response, object userstate)
        {
            _oAuthToken = GetQueryParameter(response.Content, "oauth_token");
            _oAuthTokenSecret = GetQueryParameter(response.Content, "oauth_token_secret");
            var authorizeUrl = TwitterSettings.AuthorizeUri + "?oauth_token=" + _oAuthToken;
            if (String.IsNullOrEmpty(_oAuthToken) || String.IsNullOrEmpty(_oAuthTokenSecret))
            {
                Dispatcher.BeginInvoke(() => MessageBox.Show("error calling twitter"));
                return;
            }
            Dispatcher.BeginInvoke(() => BrowserControl.Navigate(new Uri(authorizeUrl)));
        }
        private static string GetQueryParameter(string input, string parameterName)
        {
            foreach (string item in input.Split('&'))
            {
                var parts = item.Split('=');
                if (parts[0] == parameterName)
                {
                    return parts[1];
                }
            }
            return String.Empty;
        }
        private void BrowserControl_Navigated(object sender, NavigationEventArgs e)
        {
            ProgressBar.IsIndeterminate = false;
            ProgressBar.Visibility = Visibility.Collapsed;
        }
        private void BrowserControl_Navigating(object sender, NavigatingEventArgs e)
        {
            ProgressBar.IsIndeterminate = true;
            ProgressBar.Visibility = Visibility.Visible;
            if (e.Uri.AbsoluteUri.CompareTo("https://api.twitter.com/oauth/authorize") == 0)
            {
                ProgressBar.IsIndeterminate = true;
                ProgressBar.Visibility = Visibility.Visible;
            }
            if (!e.Uri.AbsoluteUri.Contains(TwitterSettings.CallbackUri))
                return;
            e.Cancel = true;
            var arguments = e.Uri.AbsoluteUri.Split('?');
            if (arguments.Length < 1)
                return;
            GetAccessToken(arguments[1]);
        }
        private void GetAccessToken(string uri)
        {
            var requestToken = GetQueryParameter(uri, "oauth_token");
            if (requestToken != _oAuthToken)
            {
                MessageBox.Show("Twitter auth tokens don't match");
            }
            var requestVerifier = GetQueryParameter(uri, "oauth_verifier");
            var credentials = new OAuthCredentials
            {
                Type = OAuthType.AccessToken,
                SignatureMethod = OAuthSignatureMethod.HmacSha1,
                ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
                ConsumerKey = TwitterSettings.ConsumerKey,
                ConsumerSecret = TwitterSettings.ConsumerKeySecret,
                Token = _oAuthToken,
                TokenSecret = _oAuthTokenSecret,
                Verifier = requestVerifier
            };
            var client = new RestClient
            {
                Authority = "https://api.twitter.com/oauth",
                Credentials = credentials,
                HasElevatedPermissions = true
            };
            var request = new RestRequest
            {
                Path = "/access_token"
            };
            client.BeginRequest(request, new RestCallback(RequestAccessTokenCompleted));
        }
        private void RequestAccessTokenCompleted(RestRequest request, RestResponse response, object userstate)
        {
            var twitteruser = new TwitterAccess
            {
                AccessToken = GetQueryParameter(response.Content, "oauth_token"),
                AccessTokenSecret = GetQueryParameter(response.Content, "oauth_token_secret"),
                UserId = GetQueryParameter(response.Content, "user_id"),
                ScreenName = GetQueryParameter(response.Content, "screen_name")
            };
            if (String.IsNullOrEmpty(twitteruser.AccessToken) || String.IsNullOrEmpty(twitteruser.AccessTokenSecret))
            {
                Dispatcher.BeginInvoke(() => MessageBox.Show(response.Content));
                return;
            }
            Helper.SaveSetting(Constants.TwitterAccess, twitteruser);
            Dispatcher.BeginInvoke(() =>
            {
                if (NavigationService.CanGoBack)
                {
                    NavigationService.GoBack();
                }
            });
        }
    }
}
There are a few things to note above.  The OnNavigatedTo method is invoked whenever this xaml page is Navigated to.  This method calls GetTwitterToken. 
You can then see the magic of Hammock in wrapping the REST calls to Twitter.  Notice the request object sets the path to “/request_token”.  It’s requesting a token from Twitter.  Once completed, the TwitterRequestTokenCompleted method is invoked where we extract the auth token and auth secret.  The return method will then navigate to a new Twitter Authorize URL in the browser control with these tokens attached.  If all is good it will redirect us back to http://www.bing.com with a new token attached as a query string parameter.
In BrowserControl_Navigating we check to see if we are navigating to our callback URI if so we cancel the navigation, grab our token and make a final call to Twitter for the users Access Token.
Finally in RequestAccessTokenCompleted we grab the Access Token and Secret and store them in a new object called TwitterAccess.  This was also defined in the TwitterSettings.cs file, followed by saving this object to Isolated Storage.
There’s still one Helper class we need to create that will help load and store our Isolated Storage files.  We need to store our user tokens in Isolated Storage so that we don’t have to get them again.
Create another new class under the “Common” folder called Helper.cs
image
This class is going to use “DataContractSerializer” which requires us to add a new reference to “Systm.Runtime.Serialization
image
Here’s the code for Helper.cs file:
using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
using System.Windows;
namespace Twitt.Common
{
    public static class Helper
    {
        private static Object _thisLock = new Object();
        public static T LoadSetting<T>(string fileName)
        {
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (!store.FileExists(fileName))
                    return default(T);
                lock (_thisLock)
                {
                    try
                    {
                        using (var stream = store.OpenFile(fileName, FileMode.Open, FileAccess.Read))
                        {
                            var serializer = new DataContractSerializer(typeof(T));
                            return (T)serializer.ReadObject(stream);
                        }
                    }
                    catch (SerializationException se)
                    {
                        Deployment.Current.Dispatcher.BeginInvoke(
                            () => MessageBox.Show(String.Format("Serialize file error {0}:{1}", se.Message, fileName)));
                        return default(T);
                    }
                    catch (Exception e)
                    {
                        Deployment.Current.Dispatcher.BeginInvoke(
                            () => MessageBox.Show(String.Format("Load file error {0}:{1}", e.Message, fileName)));
                        return default(T);
                    }
                }
            }
        }
        public static void SaveSetting<T>(string fileName, T dataToSave)
        {
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                lock (_thisLock)
                {
                    try
                    {
                        using (var stream = store.CreateFile(fileName))
                        {
                            var serializer = new DataContractSerializer(typeof(T));
                            serializer.WriteObject(stream, dataToSave);
                        }
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show(String.Format("Save file error {0}:{1}", e.Message, fileName));
                        return;
                    }
                }
            }
        }
        public static void DeleteFile(string fileName)
        {
            using (var store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (store.FileExists(fileName))
                    store.DeleteFile(fileName);
            }
        }
        public static DateTime ParseDateTime(string date)
        {
            var dayOfWeek = date.Substring(0, 3).Trim();
            var month = date.Substring(4, 3).Trim();
            var dayInMonth = date.Substring(8, 2).Trim();
            var time = date.Substring(11, 9).Trim();
            var offset = date.Substring(20, 5).Trim();
            var year = date.Substring(25, 5).Trim();
            var dateTime = string.Format("{0}-{1}-{2} {3}", dayInMonth, month, year, time);
            var ret = DateTime.Parse(dateTime).ToLocalTime();
            return ret;
        }
        public static void ShowMessage(string message)
        {
            Deployment.Current.Dispatcher.BeginInvoke(() => MessageBox.Show(message));
        }
    }
}
There are a couple other helper methods in here we’ll be using later, like DeleteFile, LoadSetting and ParseDateTime.  One other note: you may notice I have a thread lock around reading and writing my files.  I needed thread safety and just threw a quick lock around them.  I will change this to a reader-writer lock in the future, but this should do for now.
We’ll need one more Constants class to make this all work.  Create another new class under the “Common” folder called “Constants.cs” and add the following:
namespace Twitt.Common
{
    public class Constants
    {
        public static string TwitterAccess = "TwitterAccess";
    }
}
This will be used for our file names.
You should be able to compile and run now.  Then navigate to our new page, you should get a Twitter web page in the browser control:
image
Enter in some valid credentials, then hit the “Allow” link.  If the call was successful you’ll see this screen:
image
You don’t want to hit the back button here since we’re not done.  When authentication is complete the code will go back for us to our settings panorama item.

image
We are now authorized and can go ahead and start reading lists and sending Tweets.

Part 3 will show how to send a Tweet.
Download the source code for part 2 here: http://twitt.codeplex.com/

Sam

10 comments:

  1. Great!
    I'll be waiting for the part 3

    ReplyDelete
  2. The souce code can now be found at http://twitt.codeplex.com/

    ReplyDelete
  3. Hi Sam, i build a WP7 app according to your steps, when i click the button "Account Settings" i meet a error "error calling twitter", then i goto http://twitt.codeplex.com/ to download the part 2 code, and run it, but it still show a "error calling twitter".
    Could you give me some advice?
    Thanks!

    ReplyDelete
  4. I found the mistake: missed a '!'!
    A miss is as good as a mile. T_T

    ReplyDelete
  5. hello sam, when authentication is done it takes me to the callbackurl, i dont understand why, what am i doing wrong?

    ReplyDelete
  6. It should take you to the callback URL, which is correct. That's why you need to trap the navigation to it and override it.

    ReplyDelete
  7. Hi Sam, i have the same problem "error calling twitte"!! and i don't unterstand the solution found !.Please help..
    Thanks

    ReplyDelete
  8. Hi Sam, Even I am facing the same problem " error calling twitter" Can you please help me out ..

    ReplyDelete
  9. It claims I can't add hammock cause it wasn't built using the WP runtime.

    ReplyDelete
  10. HI,
    iam not able to come back to the mainpage i.e account seetings page..can u tel me where i did mistake..

    ReplyDelete