YouTube and FourSquare from a Windows Phone App

Tweet

The last couple of sites that I want to cover in this series on authentication from your Windows Phone application are YouTube and FourSquare. The former uses OAuth 1, whilst the latter uses OAuth 2.

YouTube (i.e. Google).

I’m not going to bore you by going through each step, since we’ve already covered a number of sites that use OAuth 1. However, there are a few little pointers that I want to highlight. The first is that rather than going to YouTube to register your application, you actually need to head to Google and specifically to the page where you can manage your Google domains.

In order to access any of the Google APIs you need to register the domain of the site where your web application will be located. Of course, we’re not going to be building a web application, so you just need to give it a domain that you own (in our case www.builttoroam.com). Once you’re done you should see a page similar to Figure 1 which includes your OAuth Consumer Key and OAuth Consumer Secret.

YouTube & FourSquare Figure 1

Figure 1

Equipped with our key and secret we’re ready to go through the OAuth process. We’re going to use the same page layout we’ve used in previous posts, and the code is pretty much the same. Here’s all the code for performing the authentication (to save you jumping to those previous posts).

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 = "https://www.google.com/accounts/OAuthGetRequestToken";
private const string AuthorizeUrl = "https://www.google.com/accounts/OAuthAuthorizeToken";
private const string AccessUrl = "https://www.google.com/accounts/OAuthGetAccessToken";

private string token;
private string tokenSecret;
private string pin;

private void AuthenticateClick(object sender, RoutedEventArgs e) {
    var parameters = new Dictionary<string, string>() { {"scope", "http://gdata.youtube.com" } };

    // Create the Request
    var request = CreateRequest("GET", RequestUrl,parameters);
    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(Exception ex) {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Unable to retrieve request token"));
        }
    }, request);
}

private const string OAuthVersion = "1.0";
private const string Hmacsha1SignatureType = "HMAC-SHA1";
private const string ConsumerKey = "<your_consumer_key>";
private const string ConsumerSecret = "<your_consumer_secret>";

private WebRequest CreateRequest(string httpMethod, string requestUrl, IDictionary<string, string> requestParameters = null) {
    if (requestParameters == null) {
        requestParameters = new Dictionary<string, 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;
    return request;
}

public string GenerateSignature(string httpMethod, string normalizedUrl, 
                                                           string queryString, IDictionary<string, 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() {
    // 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, IDictionary<string, string> requestParameters) {
    var parameters = new List<KeyValuePair<string, string>>(GetQueryParameters(queryString)) {
                                new KeyValuePair<string, string>(OAuthVersionKey, requestParameters[OAuthVersionKey]),
                                new KeyValuePair<string, string>(OAuthNonceKey, requestParameters[OAuthNonceKey]),
                                new KeyValuePair<string, string>(OAuthTimestampKey,
                                                                requestParameters[OAuthTimestampKey]),
                                new KeyValuePair<string, string>(OAuthSignatureMethodKey,
                                                                requestParameters[OAuthSignatureMethodKey]),
                                new KeyValuePair<string, string>(OAuthConsumerKeyKey,
                                                                requestParameters[OAuthConsumerKeyKey])
                            };

    if (requestParameters.ContainsKey(OAuthVerifierKey)) {
        parameters.Add(new KeyValuePair<string, string>(OAuthVerifierKey, requestParameters[OAuthVerifierKey]));
    }

    if (requestParameters.ContainsKey(OAuthTokenKey)) {
        parameters.Add(new KeyValuePair<string, string>(OAuthTokenKey, requestParameters[OAuthTokenKey]));
    }

    foreach (var kvp in requestParameters) {
        if (kvp.Key.StartsWith("oauth_") || kvp.Key == OAuthPostBodyKey) continue;
        parameters.Add(new KeyValuePair<string, string>(kvp.Key,UrlEncode(kvp.Value)));
    }

    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 IEnumerable<KeyValuePair<string, string>> GetQueryParameters(string queryString) {
    var parameters = new List<KeyValuePair<string, 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 KeyValuePair<string, string>(bits[0], bits[1])).ToArray();
}


private static string BuildParameterString(IEnumerable<KeyValuePair<string, 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));

    // 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);
}

private IEnumerable<KeyValuePair<string, 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 KeyValuePair<string, 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;
}

private void BrowserNavigated(object sender, System.Windows.Navigation.NavigationEventArgs e) {
    if (AuthenticationBrowser.Visibility == Visibility.Collapsed) {
        AuthenticationBrowser.Visibility = Visibility.Visible;
    }
    if (e.Uri.AbsoluteUri.ToLower().Contains("oauth_token_authorized")) {
        // The request token is now "authorized", so just convert it to the access token
        RetrieveAccessToken();
        AuthenticationBrowser.Visibility = Visibility.Collapsed;
    }
}

public void RetrieveAccessToken() {
    var request = CreateRequest("GET", 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();

                ExtractTokenInfo(responseText);
                RetrieveProfile();
            }
        }
        catch
        {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Unable to retrieve Access Token"));
        }
    }, request);
}

private void RetrieveProfile() {
    var request = CreateRequest("GET",
                            "https://gdata.youtube.com/feeds/api/users/default?alt=json");
    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()) {
                var serializer = new DataContractJsonSerializer(typeof(YouTubeProfileResponse));
                var profileResponse = serializer.ReadObject(strm) as YouTubeProfileResponse;

                Dispatcher.BeginInvoke(() => {
                    MessageBox.Show("Access granted");

                    UserIdText.Text = profileResponse.Entry.Id.Text;
                    UserNameText.Text =profileResponse.Entry.Username.Text;
                });
            }
        }
        catch {
            Dispatcher.BeginInvoke(() => MessageBox.Show("Unable to retrieve Access Token"));
        }
    }, request);
}

[DataContract]
public class YouTubeProfileResponse {
    [DataMember(Name = "entry")]
    public YouTubeEntry Entry { get; set; }

    [DataContract]
    public class YouTubeEntry {
        [DataMember(Name="id")]
        public GoogleText Id { get; set; }

        [DataMember(Name="yt$username")]
        public GoogleText Username { get; set; }
    }

    [DataContract]
    public class GoogleText {
        [DataMember(Name="$t")]
        public string Text { get; set; }
    }
}

You’ll notice that we’re using HTTP GET requests and that the parameters (including the OAuth parameters and signature) are part of the URL, rather than the Authorization header. Google actually support a number of different mechanisms, so you can choose which one best suits.

The other thing you might have noticed is that when requesting the user’s profile the URL ends in alt=json. This is requesting the response in JSON, instead of the default XML. We’ve chosen to use the JSON format as it is more compact and generally quicker to parse than the equivalent XML. If you run this code you should see an authentication sequence similar to Figure 2.

YouTube & FourSquare Figure 2

Figure 2

FourSquare

The nice thing about FourSquare is that we’re back in OAuth 2-land, which makes everything much simpler. Of course the first thing to do is to register an application. After you’ve logged in, navigate to FourSquare OAuth where you can complete the information about your application (Figure 3).

YouTube & FourSquare Figure 3

Figure 3

As we’re building a mobile application you’d have thought we wouldn’t need a callback URL. Unfortunately it is a required parameter, as is application web site. Just use a domain that you have and a callback URL that you know does not exist. At the end of the authentication/authorization process the web browser will be redirected back to this callback URL. However, this navigation will be intercepted by your application so that you can parse off the access token. The user will never see that the web browser fails to navigate to this page.

Once you’re registered you should see a page similar to Figure 4, which includes the Client ID and a Client Secret. You’ll only need the Client ID for the authentication/authorization process.

YouTube & FourSquare Figure 4

Figure 4

So, all that’s left is the code to implement the authentication/authorization process. As we’ve already gone through this (see the Facebook post) I’ll simply include the worked code here for your reference.

private void AuthenticateClick(object sender, RoutedEventArgs e) {
    var uriParams = new Dictionary<string, string>() {
                {"client_id", "<your_client_ID>"},
                {"response_type", "token"},
                {"scope", "user_about_me, offline_access, publish_stream"},
                {"redirect_uri", "<your_callback_URL>"},
                {"display", "touch"}
            };

    StringBuilder urlBuilder = new StringBuilder();
    foreach (var current in uriParams) {
        if (urlBuilder.Length > 0) {
            urlBuilder.Append("&");
        }
        var encoded = HttpUtility.UrlEncode(current.Value);
        urlBuilder.AppendFormat("{0}={1}", current.Key, encoded);
    }
    var loginUrl = "https://foursquare.com/oauth2/authenticate?" + urlBuilder.ToString();
    AuthenticationBrowser.Navigate(new Uri(loginUrl));
    AuthenticationBrowser.Visibility = Visibility.Visible;
}

public string AccessToken { get; set; }
private void BrowserNavigated(object sender, NavigationEventArgs e) {
    if (string.IsNullOrEmpty(e.Uri.Fragment)) return;
    if (e.Uri.AbsoluteUri.ToLower().StartsWith("<your_callback_URL>")) {
        string text = HttpUtility.HtmlDecode(e.Uri.Fragment).TrimStart('#');
        var pairs = text.Split('&');
        foreach (var pair in pairs) {
            var kvp = pair.Split('=');
            if (kvp.Length == 2) {
                if (kvp[0] == "access_token") {
                    AccessToken = HttpUtility.UrlDecode(kvp[1]);
                    MessageBox.Show("Access granted");
                    RequestUserProfile();
                }
            }
        }

        if (string.IsNullOrEmpty(AccessToken)) {
            MessageBox.Show("Unable to authenticate");
        }

        AuthenticationBrowser.Visibility = System.Windows.Visibility.Collapsed;
    }
}

private void RequestUserProfile() {
    var profileUrl = string.Format("https://api.foursquare.com/v2/users/self?oauth_token={0}",
                                                            HttpUtility.UrlEncode(AccessToken));

    var request = HttpWebRequest.Create(new Uri(profileUrl));
    request.Method = "GET";
    request.BeginGetResponse(result => {
        try
        {
            var resp = (result.AsyncState as HttpWebRequest).EndGetResponse(result);
            using (var strm = resp.GetResponseStream()) {
                var serializer = new DataContractJsonSerializer(typeof(FourSquareProfileResponse));
                var profile = serializer.ReadObject(strm) as FourSquareProfileResponse;
                this.Dispatcher.BeginInvoke(
                    (Action<FourSquareProfileResponse>)((user) => {
                        this.UserIdText.Text = user.Response.User.Id;
                        this.UserNameText.Text = user.Response.User.FirstName + " " + user.Response.User.LastName;
                    }), profile);
            }
        }
        catch (Exception ex) {
            this.Dispatcher.BeginInvoke(() =>
                MessageBox.Show("Unable to attain profile information"));
        }
    }, request);
}

[DataContract]
public class FourSquareProfileResponse {
    [DataMember(Name = "response")]
    public ProfileResponse Response { get; set; }

    [DataContract]
    public class ProfileResponse {
        [DataMember(Name="user")]
        public ResponseUser User { get; set; }

        [DataContract]
        public class ResponseUser {
            [DataMember(Name = "id")]
            public string Id { get; set; }
            [DataMember(Name = "firstName")]
            public string FirstName { get; set; }
            [DataMember(Name = "lastName")]
            public string LastName { get; set; }
        }
    }

}

private void PostStatusUpdate(string status, Action<bool, Exception> callback) {
    var request = HttpWebRequest.Create("https://api.foursquare.com/v2/checkins/add");
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";
    request.BeginGetRequestStream((reqResult) => {
        using (var strm = request.EndGetRequestStream(reqResult))
        using (var writer = new StreamWriter(strm)) {
            writer.Write("oauth_token=" + AccessToken);
            writer.Write("&shout=" + HttpUtility.UrlEncode(status));
            writer.Write("&broadcast=private");
        }

        request.BeginGetResponse((result) => {
            try {
                var response = request.EndGetResponse(result);
                using (var rstrm = response.GetResponseStream()) {
                    using (var reader = new StreamReader(rstrm))
                    {
                        var txt = reader.ReadToEnd();
                    }
                    callback(true, null);
                }
            }
            catch (Exception ex) {
                callback(false, ex);
            }
        }, null);
    }, null);
}

private void PostUpdateClick(object sender, RoutedEventArgs e) {
    PostStatusUpdate(this.StatusText.Text, (success, ex) => {
        this.Dispatcher.BeginInvoke(() => {
            if (success && ex == null) {
                MessageBox.Show("Status updated");
            }
            else {
                MessageBox.Show("Unable to update status");
            }
        });
    });
}

So there you have it. If you’ve been following along with this series you’ll have seen how we can use a very similar approach to authenticating using OAuth 1 or 2 across a variety of different social networking and new media web sites. I’m sure there are plenty of sites that we haven’t covered here – if there is, and you’d like me to take a look, leave a comment below.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://nicksnettravels.builttoroam.com Nick

    Update: For some reason the Redirect Url now needs to exist in order for FourSquare authentication to work (contrary to their documentation)