Zend certified PHP/Magento developer

BuildMobile: Flickr from a Windows Phone App

The next cab off the authentication rank is Flickr. In the preceding posts you’ll have already seen a couple of implementations of OAuth 1. Unfortunately whilst Flickr claims that they implement OAuth Core 1.0 Revision A it would appear that this is not the case for Windows Phone applications. Instead you currently need to use their legacy authentication process. This will give you an authentication token which you can use to make calls against the Flickr API. Each call you make, you will also need to authenticate using this token, again using the legacy authentication process. This has been well documented by Dennis Delimarsky in his series on working with Flickr.

Rather than simply duplicate Dennis’ work in this post I want to show how, once you have a legacy authentication token, you can exchange it for an OAuth token. This means that you can make all your service calls against Flickr using their new authentication system. In the future, when Flickr sort out their OAuth implementation, all you’ll have to do is swap out the initial authentication, and replace it with your OAuth code.

Step 1: Sign up for a Flickr Application

Let’s get cracking – first up, you’ll of course have to register an application with Flickr. In addition to their OAuth implementation not functioning correctly, their documentation is a pigs-breakfast and very difficult to find what you want. Start by going to http://developer.flickr.com. From there, click the API link under the banner, followed by the Request an API key link, which should be step 1 under Getting Started. We’re almost there; click the Request an API Key link (yes, it’s another link with the same name, this time on the The App Garden page). If you get lost, go directly to http://www.flickr.com/services/apps/create/apply to register your application.

Flickr offer either a non-commercial or a commercial registration process. The big difference in the registration process is that the commercial registration may take some time to be processed (also note the warnings about completing all the fields to ensure they consider your application valid). Figure 1 illustrates the non-commercial registration for our sample application.

Flickr WP Figure1

Figure1

Once you’ve entered your application information, click the ‘Submit’ button. In the case of a non-commercial application you’ll be immediately allocated a Key and Secret, as shown in Figure 2.

Flickr WP Figure2

Figure2

There is one thing you have to change on your application in order to make it accessible you’re your Windows Phone application. Click on the Edit auth flow for this app link. This will take you to a new page where you can customise the authentication flow for your application (Figure 3).

Flickr WP Figure3

Figure3

Under App Type, select Mobile Application. Make sure you copy the authentication URL listed under Mobile Permissions as you’ll need this link to kick off the authentication process within your application. Click the ‘Save Changes’ button to commit this change.

Step 2: Create the Application

As we did in previous posts, we’ll create a simple application to demonstrate the Flickr integration. The following XAML displays a Button, to trigger the authentication process, and two TextBlock elements to display the user name and ID once authentication has been completed. One thing to note about the WebBrowser control is that there is no IsScriptEnabled attribute – it appears that setting this attribute to True causes the Flickr authentication process to fail.

phone:PhoneApplicationPage
  x:Class="FlickrTestApp.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"
                        /
  /Grid
/phone:PhoneApplicationPage

Step 3: Kick off Flickr Authentication

Now you might be guessing that this is where we’d go off to Flickr and acquire a Request Token which we’d use to create the login URL. Unfortunately if you do this the login URL will be displayed, the user will enter their credentials and then the WebBrowser goes off into la-la-land and never gets to the next page where the user is supposed to authorise the application. Instead, this is where you use the authentication URL that you wrote down in step 1.

private const string AuthenticationUrl = "http://m.flickr.com/auth-7xxxxxxxxxxxxxxxxxxx1";
private void AuthenticateClick(object sender, RoutedEventArgs e) {
    AuthenticationBrowser.Navigate(new Uri(AuthenticationUrl));
    AuthenticationBrowser.Visibility = Visibility.Visible;
}

One point to note here is that the AuthenticationUrl constant defined here uses m.flickr.com instead of www.flickr.com (i.e. change the www to m in your authentication URL). This ensures that Flickr renders a mobile page for your user to sign in to.

Step 4: User Signs In and Authorises Your Application

When the user clicks the Authenticate button (Figure 4, first image), they’ll be presented with the Yahoo/Flickr sign in page (second image), followed by the authorisation screen (third image). After clicking the ‘Ok, I’ll Authorize It’ button, they’ll see a page with a PIN number on it. This is similar to the OAuth 1 process which ends with the Verifier PIN, so you can guess what’s coming next…

Flickr WP Figure4a

Figure4a

Flickr WP Figure4b

Figure4b

Step 5: Extract the Mini-Token (PIN)

In the BrowserNavigated method (which is called every time the browser has navigated to a page), you need to extract out the mini-token (i.e. the PIN, shown in Figure 4). The following code again uses a regular expression to extract out the mini-token.

private const string RedirectUrl = "http://m.flickr.com/services/auth/";
private string miniToken;
private void BrowserNavigated(object sender, NavigationEventArgs e) {
    if (e.Uri.AbsoluteUri.ToLower() == RedirectUrl) {
        var htmlString = AuthenticationBrowser.SaveToString();
        var pinFinder = new Regex(@"(?minitoken[A-Za-z0-9-]+)/span", RegexOptions.IgnoreCase);
        var match = pinFinder.Match(htmlString);
        if (match.Length  0) {
            var group = match.Groups["minitoken"];
            if (group.Length  0){
                miniToken = group.Captures[0].Value;
                if (!string.IsNullOrEmpty(miniToken)) {
                    RetrieveAuthenticationToken ();
                }
            }
        }
        if (string.IsNullOrEmpty(miniToken)) {
            Dispatcher.BeginInvoke(() = MessageBox.Show("Authorization denied by user"));
        }
        // Make sure PIN is reset to null
        miniToken = null;
        AuthenticationBrowser.Visibility = Visibility.Collapsed;
    }
}

Step 6: Retrieve Authentication Token

The mini-token can’t itself be used to authenticate calls made to the Flickr APIs. Instead you need to convert it into an authentication token by calling the flickr.auth.getFullToken method. To do this you’ll need to generate the appropriate web request which will be made up of the method name, the parameters and a signature. The signature needs to be a MD5 hash of the request parameters.

Unfortunately Windows Phone doesn’t have an implementation of MD5. Luckily there is one that is available at http://archive.msdn.microsoft.com/SilverlightMD5. Go to the Downloads tab and download the MD5.cs file into your application.

Important: There is, at time of writing, a bug on line 109 of this file. Replace if (cbSize = 56) with if (cbSize 56) as suggested by the comments on the Home page of this site.

The process of creating the web request is then relatively simple as the RequestAuthenticationToken method shows. Don’t forget to update the ConsumerKey and ConsumerSecret with your Key and Secret values from Step 1.

private const string ConsumerKey = "your key for your flickr application";
private const string ConsumerSecret = "your secret for your flickr application";
private const string Flickr_GetFullToken = "flickr.auth.getFullToken";
private const string MethodKey = "method";
private const string ApiKeyKey = "api_key";
private const string MiniTokenKey = "mini_token";
private const string ApiSignatureKey = "api_sig";
private void RetrieveAuthenticationToken() {
    var parameters = new Dictionarystring, string();
    parameters[MethodKey] = Flickr_GetFullToken;
    parameters[ApiKeyKey] = ConsumerKey;
    parameters[MiniTokenKey] = miniToken;
    parameters[ApiSignatureKey] = GenerateSignature(parameters);
    var queryString = BuildParameterString(parameters);
    var reqUrl = "http://api.flickr.com/services/rest/?" + queryString;
    var request = WebRequest.CreateHttp(reqUrl);
    request.BeginGetResponse(result = {
        var req = result.AsyncState as HttpWebRequest;
        using (var resp = req.EndGetResponse(result))
        using (var strm = resp.GetResponseStream()) {
            var xml = XElement.Load(strm);
            var authenticationToken = xml.Descendants("token").Select(x = x.Value).FirstOrDefault();
            RetrieveAccessToken(authenticationToken);
        }
    }, request);
}
private string GenerateSignature(IDictionarystring,string queryParameters) {
    var parameterList = new ListKeyValuePairstring, string(queryParameters);
    parameterList.Sort((kvp1, kvp2) = {
        if (kvp1.Key == kvp2.Key) {
            return string.Compare(kvp1.Value, kvp2.Value);
        }
        return string.Compare(kvp1.Key, kvp2.Key);
    });
    var signatureBase = new StringBuilder(ConsumerSecret);
    foreach (var kvp in parameterList) {
        signatureBase.Append(kvp.Key + kvp.Value);
    }
    return MD5Core.GetHashString(signatureBase.ToString());
}
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();
}

Step 7: Convert Authentication Token to Access Token

In the previous step you’ll have noticed that once we’ve retrieved the authentication token there is a call to RetrieveAccessToken. This is the process by which we exchange the authentication token (i.e. the legacy authentication system) with an OAuth Access Token (and corresponding token secret). It’s simply a method call to the flickr.auth.oauth.getAccessToken method, as follows:

private const string Flickr_GetAccessToken = "flickr.auth.oauth.getAccessToken";
private const string AuthenticationTokenKey = "auth_token";
private string token;
private string tokenSecret;
private void RetrieveAccessToken(string authenticationToken) {
    var parameters = new Dictionarystring, string();
    parameters[MethodKey] = Flickr_GetAccessToken;
    parameters[ApiKeyKey] = ConsumerKey;
    parameters[AuthenticationTokenKey] = authenticationToken;
    parameters[ApiSignatureKey] = GenerateSignature(parameters);
    var queryString = BuildParameterString(parameters);
    var reqUrl = "http://api.flickr.com/services/rest/?" + queryString;
    var request = WebRequest.CreateHttp(reqUrl);
    request.BeginGetResponse(result = {
                                        var req =result.AsyncState as HttpWebRequest;
                                        using (var resp = req.EndGetResponse(result))
                                        using (var strm = resp.GetResponseStream()) {
                                            var xml = XElement.Load(strm);
                                            var access =
                                                xml.Descendants("access_token").FirstOrDefault();
                                            token = access.Attribute("oauth_token").Value;
                                            tokenSecret = access.Attribute("oauth_token_secret").Value;
                                            RetrieveProfile();
                                        }
                                    }, request);
}

Step 8: Retrieve User’s Profile

Now in actual fact if you look at the XML response that comes back along with the authentication token you’ll notice that the user’s ID is included in this response. However, this is not the case with the OAuth implementation so to make this approach future proof we’ll call the flickr.test.login method, which will return information about the authenticated user. Now that we have the OAuth Access Token and Secret we’re back on familiar territory in that we simply need to create the corresponding request in a similar manner to what we’ve done in the previous posts covering OAuth.

private const string Flickr_TestLogin = "flickr.test.login";
private const string NoJsonCallBackKey = "nojsoncallback";
private const string FormatKey = "format";
private const string NoCallBack = "1";
private const string JsonResponseFormat = "json";
private void RetrieveProfile() {
    var requestParameters = new Dictionarystring, string();
    requestParameters[NoJsonCallBackKey] = NoCallBack;
    requestParameters[FormatKey] = JsonResponseFormat;
    requestParameters[MethodKey] = Flickr_TestLogin;
    var profileUrl = "http://api.flickr.com/services/rest";
    var request = CreateRequest("GET", profileUrl, requestParameters);
    request.BeginGetResponse(result = {
        try {
            var req2 = result.AsyncState as HttpWebRequest;
            if (req2 == null) throw new ArgumentNullException("result", "Request parameter is null");
            using (var resp = req2.EndGetResponse(result))
            using (var strm = resp.GetResponseStream()) {
                var serializer = new DataContractJsonSerializer(typeof(FlickrLoginResponse));
                var user = serializer.ReadObject(strm) as FlickrLoginResponse;
                Dispatcher.BeginInvoke(() = {
                                                    UserIdText.Text = user.User.Id;
                                                    UserNameText.Text = user.User.Username.Name;
                                                });
            }
        }
        catch (Exception ex) {
            Dispatcher.BeginInvoke(() = MessageBox.Show("Unable to access profile"));
        }
    }, request);
}
[DataContract]
public class FlickrLoginResponse {
    [DataMember(Name = "user")]
    public FlickrUser User { get; set; }
    [DataContract]
    public class FlickrUser {
        [DataMember(Name = "id")]
        public string Id { get; set; }
        [DataMember(Name = "username")]
        public FlickrUsername Username { get; set; }
        [DataContract]
        public class FlickrUsername {
            [DataMember(Name = "_content")]
            public string Name { get; set; }
        }
    }
}

Now, that’s pretty much it. To ensure that you don’t get messed up working out which previous post to copy the OAuth implementation from (not to mention the couple of little adjustments needed to support Flickr’s implementation), I’ve included the rest of the OAuth code at the end of this post. We’re almost at the end of this series on authentication posts – if there is a social network that we haven’t covered and that you’d like to see in this series, make sure you drop down a comment.

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 OAuthVersion = "1.0";
private const string Hmacsha1SignatureType = "HMAC-SHA1";
private string pin;
private WebRequest CreateRequest(string httpMethod, string requestUrl, IDictionarystring, string requestParameters = null)
{
if (requestParameters == null)
{
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 sb = new StringBuilder();
sb.Append(url.Query);
foreach (var param in requestParameters)
{
if (sb.Length  0) sb.Append("");
sb.Append(string.Format("{0}={1}", param.Key, param.Value));
}
if (sb[0] != '?') sb.Insert(0, "?");
var request = WebRequest.CreateHttp(normalizedUrl + sb.ToString());
request.Method = httpMethod;
request.Headers[HttpRequestHeader.Authorization] = GenerateAuthorizationHeader(requestParameters);
return request;
}
public string GenerateSignature(string httpMethod, string normalizedUrl, string queryString, IDictionarystring, string requestParameters, string secret = null)
{
requestParameters["oauth_callback"] = "oob";
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()
{
// Just a simple implementation of a random number between 123400 and 9999999
return Random.Next(123400, 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]),
new KeyValuePairstring, string("oauth_callback",
requestParameters["oauth_callback"])
};
if (requestParameters.ContainsKey(OAuthVerifierKey))
{
parameters.Add(new KeyValuePairstring, string(OAuthVerifierKey, requestParameters[OAuthVerifierKey]));
}
if (requestParameters.ContainsKey(OAuthTokenKey))
{
parameters.Add(new KeyValuePairstring, string(OAuthTokenKey, requestParameters[OAuthTokenKey]));
}
foreach (var kvp in requestParameters)
{
if (kvp.Key.StartsWith("oauth_") || kvp.Key == OAuthPostBodyKey) continue;
parameters.Add(kvp);
}
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();
}
/// 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));
// Upgrade the escaping to RFC 3986, if necessary.
foreach (string t in UriRfc3986CharsToEscape)
{
escaped.Replace(t, HexEscape(t[0]));
}
// Return the fully-RFC3986-escaped string.
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);
}
public static string GenerateAuthorizationHeader(IDictionarystring, string requestParameters)
{
var paras = new StringBuilder();
foreach (var param in requestParameters)
{
if (!param.Key.StartsWith("oauth_")) continue;
if (paras.Length  0) paras.Append(",");
paras.Append(param.Key + "="" + param.Value + """);
}
return "OAuth " + paras;
}
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;
}