Статьи

YouTube и FourSquare из приложения для Windows Phone

Последние пару сайтов, которые я хочу охватить в этой серии по аутентификации из приложения Windows Phone, — это YouTube и FourSquare . Первый использует OAuth 1, а второй использует OAuth 2.

YouTube (то есть Google).

Я не буду утомлять вас, пройдя каждый шаг, поскольку мы уже рассмотрели ряд сайтов, использующих OAuth 1. Однако есть несколько небольших указателей, которые я хочу выделить. Во-первых, вместо того, чтобы заходить на YouTube, чтобы зарегистрировать свое приложение, вам действительно нужно перейти в Google и, в частности, на страницу, где вы можете управлять своими доменами Google .

Для доступа к любому из API Google вам необходимо зарегистрировать домен сайта, на котором будет размещаться ваше веб-приложение. Конечно, мы не собираемся создавать веб-приложение, поэтому вам просто нужно дать ему домен, которым вы владеете (в нашем случае www.builttoroam.com). Когда вы закончите, вы увидите страницу, похожую на рисунок 1, которая включает ваш ключ потребителя OAuth и секретный ключ OAuth.

YouTube & FourSquare Рисунок 1

фигура 1

С нашим ключом и секретом мы готовы пройти через процесс OAuth. Мы собираемся использовать тот же макет страницы, который мы использовали в предыдущих постах, и код почти такой же. Вот весь код для выполнения аутентификации (чтобы спасти вас от перехода на предыдущие посты).

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

Вы заметите, что мы используем HTTP-запросы GET и что параметры (включая параметры OAuth и подпись) являются частью URL, а не заголовком Authorization. Google на самом деле поддерживает ряд различных механизмов, поэтому вы можете выбрать, какой из них лучше всего подходит.

Другая вещь, которую вы могли заметить, это то, что при запросе профиля пользователя URL-адрес заканчивается на alt=json Это запрашивает ответ в формате JSON вместо XML по умолчанию. Мы решили использовать формат JSON, так как он более компактен и, как правило, быстрее анализируется, чем эквивалентный XML. Если вы запустите этот код, вы должны увидеть последовательность аутентификации, подобную изображенной на рисунке 2.

YouTube & FourSquare Рисунок 2

фигура 2

FourSquare

Хорошая вещь о FourSquare состоит в том, что мы вернулись в OAuth 2-land, что делает все намного проще. Конечно, первое, что нужно сделать, это зарегистрировать приложение. После того, как вы вошли в систему, перейдите к FourSquare OAuth, где вы можете заполнить информацию о вашем приложении (рисунок 3).

YouTube & FourSquare Рисунок 3

Рисунок 3

Поскольку мы создаем мобильное приложение, вы бы подумали, что нам не понадобится URL обратного вызова. К сожалению, это обязательный параметр, как и веб-сайт приложения. Просто используйте домен, который у вас есть, и URL обратного вызова, который, как вы знаете , не существует . В конце процесса аутентификации / авторизации веб-браузер будет перенаправлен обратно на этот URL-адрес обратного вызова. Однако эта навигация будет перехвачена вашим приложением, чтобы вы могли проанализировать токен доступа. Пользователь никогда не увидит, что веб-браузер не может перейти на эту страницу.

Как только вы зарегистрируетесь, вы должны увидеть страницу, похожую на рисунок 4, которая включает в себя идентификатор клиента и секрет клиента. Вам потребуется только идентификатор клиента для процесса аутентификации / авторизации.

YouTube & FourSquare Рисунок 4

Рисунок 4

Итак, все, что осталось, это код для реализации процесса аутентификации / авторизации. Поскольку мы уже прошли через это (см. Пост в Facebook ), я просто включу здесь обработанный код для вашей справки.

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

Так что у вас есть это. Если вы следили за этой серией, вы увидели, как мы можем использовать очень похожий подход к аутентификации с использованием OAuth 1 или 2 в различных социальных сетях и на новых медиа-сайтах. Я уверен, что есть много сайтов, которые мы здесь не освещали — если есть, и вы хотите, чтобы я посмотрел, оставьте комментарий ниже.