Mobile Zone is brought to you in partnership with:

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Building an Imgur Client for Windows Phone - Part 4 - Authentication

02.04.2013
| 4411 views |
  • submit to reddit

If you missed the previous three parts, you can easily find them here:Today I am going to talk about user authentication, because some of the actions require the user to be logged in the service. Imgur API uses the OAuth 2.0 authentication flow (the service-specific spec is available here).

To let the user access the authentication page, you can add a button to the application bar (it is already there for your convenience in the previous source drop).

But the button has to go somewhere, so you need to create a new authentication page that would let the user enter their credentials and pass those to the Imgur service for verification. I added a new AuthPage.xaml that contains nothing but a WebBrowser component.

<phone:PhoneApplicationPage
    x:Class="Imagine.AuthPage"
    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"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d"
    shell:SystemTray.IsVisible="False">

    <!--LayoutRoot is the root grid where all page content is placed-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <phone:WebBrowser x:Name="authBrowser"></phone:WebBrowser>
    </Grid>

</phone:PhoneApplicationPage>

The way the initial authentication step is built, we need to craft a special URL that contains the client ID, as well as the client secret, that would identify the application as the source of the request. What I am going to do is navigate to this specific URL the moment the page loads, as its sole purpose is to receive the user credentials. This can be done in the AuthPage_Loaded event handler:

void AuthPage_Loaded(object sender, RoutedEventArgs e)
{
    authBrowser.Navigate(new Uri(string.Format("https://api.imgur.com/oauth2/authorize?client_id={0}&response_type=token",
        ConstantContainer.IMGUR_CLIENT_ID)));
}

Remember that internally I am keeping the client ID and secret in the ConstantContainer class. Once the page loads, you will see the prompt to enter the username and password of the user that wants to be authenticated:


You might be wondering - why am I using the token response type instead of using PIN or code. This way I can easily parse out the code from the URL without forcing the user to enter the PIN once again or perform duplicating HTTP requests. As a matter of fact, when it comes to choosing between the authorization code or the token, it comes down to personal preferences, as you will have to handle callback URL reading anyway.

Since I do not need the callback per-se, I can intercept the URL on navigation, through the Navigating event handler, and get the access and refresh tokens:

void authBrowser_Navigating(object sender, NavigatingEventArgs e)
{
    if (e.Uri.ToString().Contains("access_token="))
    {
       
        string uriString = e.Uri.ToString();
        int indexOfSharp = uriString.IndexOf("#");
        string query = uriString.Substring(indexOfSharp + 1, uriString.Length - indexOfSharp - 1);

        string accessToken = query.Split('&')
                                .Where(s => s.Split('=')[0] == "access_token")
                                .Select(s => s.Split('=')[1])
                                .FirstOrDefault();

        string refreshToken = query.Split('&')
                                .Where(s => s.Split('=')[0] == "refresh_token")
                                .Select(s => s.Split('=')[1])
                                .FirstOrDefault();

        string username = query.Split('&')
                                .Where(s => s.Split('=')[0] == "account_username")
                                .Select(s => s.Split('=')[1])
                                .FirstOrDefault();
    }
    else
    {
        Debug.WriteLine(e.Uri.ToString());
    }
}

In case you decide to use the authorization code, feel free to use my AuthHelper class:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;

namespace Imagine.Core
{
    public class AuthHelper
    {
        public static void AuthenticateCode(string code, Action<KeyValuePair<string,string>> onTokenResponseReceived)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://api.imgur.com/oauth2/token");

            string data = string.Format("client_id={0}&client_secret={1}&grant_type=authorization_code&code={2}",
                ConstantContainer.IMGUR_CLIENT_ID, ConstantContainer.IMGUR_CLIENT_SECRET, code);
            byte[] binaryDataContent = Encoding.UTF8.GetBytes(data);
            request.ContentType = "application/x-www-form-urlencoded";
            request.Method = "POST";

            request.BeginGetRequestStream(new AsyncCallback((n) =>
                {
                    using (Stream stream = (Stream)request.EndGetRequestStream(n))
                    {
                        stream.Write(binaryDataContent, 0, binaryDataContent.Length);
                    }
                    request.BeginGetResponse(new AsyncCallback((ia) =>
                        {
                            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(ia);
                            using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                            {
                                Debug.WriteLine(reader.ReadToEnd());
                            }
                        }), null);
                }),null);
        }
    }
}

At this point, I can take this a step further and have an ImgurAuthUser class that will preserve the tokens and will allow me to easily serialize and deserialize the content. Here is the class structure:

namespace Imagine.ImgurAPI
{
    public class ImgurAuthUser
    {
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
        public string Username { get; set; }
    }
}

Modifying the Navigating handler, we can get this:

void authBrowser_Navigating(object sender, NavigatingEventArgs e)
{
    if (e.Uri.ToString().Contains("access_token="))
    {
       
        string uriString = e.Uri.ToString();
        int indexOfSharp = uriString.IndexOf("#");
        string query = uriString.Substring(indexOfSharp + 1, uriString.Length - indexOfSharp - 1);

        ImgurAuthUser user = new ImgurAuthUser();

        user.AccessToken = query.Split('&')
                                .Where(s => s.Split('=')[0] == "access_token")
                                .Select(s => s.Split('=')[1])
                                .FirstOrDefault();

        user.RefreshToken = query.Split('&')
                                .Where(s => s.Split('=')[0] == "refresh_token")
                                .Select(s => s.Split('=')[1])
                                .FirstOrDefault();

        user.Username = query.Split('&')
                                .Where(s => s.Split('=')[0] == "account_username")
                                .Select(s => s.Split('=')[1])
                                .FirstOrDefault();
    }
    else
    {
        Debug.WriteLine(e.Uri.ToString());
    }
}

Great, now that I have the authentication metadata, I need to store it. To do this, I created a helper SerializeAuthMetadata function in the AuthHelper class:

/// <summary>
/// Serializes the authentication metadata returned from the service request
/// to local storage.
/// </summary>
/// <param name="user">The filled instance of the user metadata carrier class.</param>
/// <returns>If successful, is true.</returns>
public static bool SerializeAuthMetadata(ImgurAuthUser user)
{
    try
    {
        IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();
        using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("auth.xml", FileMode.Create, file))
        {
            XmlSerializer serializer = new XmlSerializer(typeof(ImgurAuthUser));
            serializer.Serialize(stream, user);
        }
        return true;
    }
    catch
    {
        return false;
    }
}

At this point, it would make sense to actually call this function when the data is stored:

if (AuthHelper.SerializeAuthMetadata(user))
{
    if (NavigationService.CanGoBack)
        NavigationService.GoBack();
}
else
{
    MessageBox.Show("Ooops! Couldn't store the authentication metadata. Make sure that you have enought free space on the phone.",
        "Imagine", MessageBoxButton.OK);
}

Once the data is successfully stored, I am simply navigating back to the main page where the images from the main gallery are displayed. But here is a problem - I still see the button that is used to log in. There are several approaches that can be taken here. I can consider it as an universal button, that would allow me to access the account information page when credentials are already available, or it could direct the user to the authentication page when those aren't. Or I could have separate buttons for account information and log out whenever credentials are stored.

I am going to go with the first scenario and repurpose it to be the "account" button instead.


For the Click event handler, I can simply check whether the auth.xml file exists - it will be deleted when the user logs out.

private void ApplicationBarIconButton_Click_1(object sender, System.EventArgs e)
{
    IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication();

    if (!file.FileExists("auth.xml"))
    {
        NavigationService.Navigate(new Uri("/AuthPage.xaml", UriKind.Relative));
    }
    else
    {
        // Placeholder for navigation to the account page.
    }
}

That's it. Now you can perform authenticated calls. As usual, you can download the latest source code drop here.