2
\$\begingroup\$

I am developing a WPF application that requires me to get an Access Token from Facebook using oAuth. After much searching online, I came to the following conclusions:

  1. OAuth must be done in a browser
  2. I need to watch the URL posts in that browser, therefore it would have to be inside a WebBrowser WPF control

I decided to create a Modal Dialog for doing the Facebook authentication, and I can just take the Access Token and ignore the rest. I wanted to keep to the MVVM model but it was more difficult than I anticipated. Any ideas on how to do that would be very helpful

Here are some features that I implemented

  • Cookie deletion so I could have another user authenticate without needing to log the current user out
  • Disable new account creation since it led to a weird UI experience
  • Listening to the cancel button from the javascript generated by Facebook

The WPF Window

The WPF is very simple. In essence it is just a WebBrowser control with the Navigated and Navigating events hooked up.

<Window x:Class="FacebookAuthenticator.FacebookAuthenticationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Authenticate Facebook" 
        Height="600" 
        Width="600" 
        ResizeMode="NoResize" 
        WindowStyle="ToolWindow">
<Grid>
    <WebBrowser Name="webBrowser" 
                Navigated="webBrowser_Navigated" 
                Navigating="webBrowser_Navigating" />
</Grid>


//The Application ID from Facebook
public string AppID {get; set; }

//The access token retrieved from facebook's authentication
public string AccessToken {get; set; }

public FacebookAuthenticationWindow()
{
    InitializeComponent();
    this.Loaded += (object sender, RoutedEventArgs e) =>
    {
        //Add the message hook in the code behind since I got a weird bug when trying to do it in the XAML
        webBrowser.MessageHook += webBrowser_MessageHook;

        //Delete the cookies since the last authentication
        DeleteFacebookCookie();

        //Create the destination URL
        var destinationURL = String.Format("https://www.facebook.com/dialog/oauth?client_id={0}&scope={1}&display=popup&redirect_uri=http://www.facebook.com/connect/login_success.html&response_type=token",
           AppID, //client_id
           "email,user_birthday" //scope
        );
        webBrowser.Navigate(destinationURL);
    };
}

Getting the Access Token

I forgot exactly where I got this code (if someone can remind me so that I could give proper credit I would be grateful).

private void webBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
    //If the URL has an access_token, grab it and walk away...
    var url = e.Uri.Fragment;
    if (url.Contains("access_token") && url.Contains("#"))
    {
        url = (new System.Text.RegularExpressions.Regex("#")).Replace(url, "?", 1);
        AccessToken = System.Web.HttpUtility.ParseQueryString(url).Get("access_token");
        DialogResult = true;
        this.Close();
    }
}

Deleting Cookies

I realized that after someone logged in, there status stayed that way and would not allow someone else to log in. I decided to remove the cookies in the beginning of each authentication in order to prevent this.

private void DeleteFacebookCookie()
{
    //Set the current user cookie to have expired yesterday
    string cookie = String.Format("c_user=; expires={0:R}; path=/; domain=.facebook.com", DateTime.UtcNow.AddDays(-1).ToString("R"));
    Application.SetCookie(new Uri("https://www.facebook.com"), cookie);
}

No New Accounts

Allowing the user to create a new account led to a weird UI experience. For my use cases, the user should already have an exsiting account. I disabled this by checking if the user was redirected to "r.php/" which is what Facebook uses to create a new account.

private void webBrowser_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e)
{
    if (e.Uri.LocalPath == "/r.php")
    {
        MessageBox.Show("To create a new account go to www.facebook.com", "Could Not Create Account", MessageBoxButton.OK, MessageBoxImage.Error);
        e.Cancel = true;
    }
}

Handling window.close()

The cancel button in Facebook's dialog shows the ability to close the window. I needed to catch this and make sure to close the window. I had no idea how but I saw that in the MessageHook I was able to see that the last message to be sent (int msg) each time was 130, so I just listened for 130. It's sloppy, but it works.

IntPtr webBrowser_MessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    //msg = 130 is the last call for when the window gets closed on a window.close() in javascript
    if (msg == 130)
    {
        this.Close();
    }
    return IntPtr.Zero;
}

Summary

Using the code is pretty simple:

FacebookAuthenticationWindow dialog = new FacebookAuthenticationWindow() { AppID = "YOURAPPID" };
if(dialog.ShowDialog() == true)
{
    string accessToken = dialog.AccessToken;
    //The world is your oyster
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I don't know much about Facebook or OAuth, so I'm not going to comment on that.

  1. this.Loaded += (object sender, RoutedEventArgs e) =>
    

    When you have a lambda that's this long, it's usually better to write it as a normal method. And when you do that, then you could also wire the event from XAML.

  2. System.Windows.Navigation.NavigationEventArgs
    System.Text.RegularExpressions.Regex
    System.Web.HttpUtility.ParseQueryString
    

    Add those namespaces as usings at the top of your file, so that you don't have to repeat them.

  3. var url = e.Uri.Fragment;
    if (url.Contains("access_token") && url.Contains("#"))
    {
        url = (new System.Text.RegularExpressions.Regex("#")).Replace(url, "?", 1);
        AccessToken = System.Web.HttpUtility.ParseQueryString(url).Get("access_token");
    

    This code is confusing (the variable url doesn't actually contain the whole url) and, if I understand it correctly, overcomplicated. First, ParseQueryString() doesn't require the leading ?. Second, if Fragment is not empty, it will always start with #. Together, this means you could just use Remove(0, 1), no need for Replace() and certainly no need to use regular expressions:

    var urlFragment = e.Uri.Fragment;
    if (urlFragment.Contains("access_token"))
    {
        var queryString = urlFragment.Remove(0, 1); // remove leading #
        AccessToken = HttpUtility.ParseQueryString(queryString).Get("access_token");
    
  4. I was able to see that the last message to be sent (int msg) each time was 130

    That seems to be the code for WM_NCDESTROY.

  5. new FacebookAuthenticationWindow() { AppID = "YOURAPPID" }
    

    Since AppID is required, it should be a constructor parameter.

\$\endgroup\$
1
  • \$\begingroup\$ WM_NCDESTROY - AWESOME! \$\endgroup\$
    – mkamioner
    Commented Dec 4, 2013 at 14:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.