DotNetOpenAuth CTP - плохой запрос Facebook

Я пытаюсь использовать CTP для подключения к Facebook через OAuth 2.0.

Я могу получить первоначальный запрос на работу в Facebook, но когда он вернется, и мы вызываем:

// Where null will become an HttpRequestInfo object
client.ProcessUserAuthorization(null);

Я получаю:

Удаленный сервер возвратил ошибку: (400) Плохой запрос.

Я не очень много сделал с исходной кодовой базой; просто установите для необязательных значений значение null (мы все еще на .NET 3.5). Любые подсказки будут высоко оценены.

Кроме того, и я предполагаю, что это больше касается Андрея; есть форум/блог для любого из этого материала или где-нибудь, что даст регулярные обновления? Было бы здорово узнать несколько вещей:

  • Плановая дата выпуска DotNetOpenAuth с OAuth 2.0
  • Будет ли .NET 4.0 предпосылкой

В любом случае, любые предложения были бы наиболее желанными.

Ответ 1

После удара этой проблемы я написал свой собственный код для авторизации и получения сведений о пользователях. Другим подходом было бы использовать Facebook С# SDK. В качестве стартера для любого, кто думает о том, чтобы делать это, вот как я это сделал. Обратите внимание, что я не рассматривал случаи ошибок.

Во-первых, читать в facebook doc о том, как это работает (его довольно просто!)

Я использую это следующим образом:

private static readonly FacebookClient facebookClient = new FacebookClient();
public ActionResult LoginWithFacebook()
{
    var result = facebookClient.Authorize();
    if (result == FacebookAuthorisationResult.RequestingCode)
    {
        //The client will have already done a Response.Redirect
        return View();
    } else if (result == FacebookAuthorisationResult.Authorized)
    {
        var user = facebookClient.GetCurrentUser();
    }
    return Redirect("/");
}

И код клиента:

using System;
using System.IO;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web;

namespace Web.Services
{
    public enum FacebookAuthorisationResult
    {
        Denied,
        Authorized,
        RequestingCode
    }
    public class FacebookClient
    {
        private const String SESSION_NAME_TOKEN = "UserFacebookToken";
        public FacebookClient()
        {
            TokenEndpoint = new Uri("https://graph.facebook.com/oauth/access_token");
            AuthorizationEndpoint = new Uri("https://graph.facebook.com/oauth/authorize");
            MeGraphEndpoint = new Uri("https://graph.facebook.com/me");
            ClientIdentifier = "xxxxxxxxxxxxxxxxxx";
            Secret = "xxxxxxxxxxxx";
            LocalSubDomain = "local.xxxxxxx.com";
        }

        public Uri TokenEndpoint { get; set; }
        public Uri AuthorizationEndpoint { get; set; }
        public Uri MeGraphEndpoint { get; set; }
        public String Secret { get; set; }
        public String ClientIdentifier { get; set; }
        private String LocalSubDomain { get; set; }


        public FacebookAuthorisationResult Authorize()
        {
            var errorReason = HttpContext.Current.Request.Params["error_reason"];
            var userDenied = errorReason != null;
            if (userDenied)
                return FacebookAuthorisationResult.Denied;
            var verificationCode = HttpContext.Current.Request.Params["code"];
            var redirectUrl = GetResponseUrl(HttpContext.Current.Request.Url);
            var needToGetVerificationCode = verificationCode == null;
            if (needToGetVerificationCode)
            {
                var url = AuthorizationEndpoint + "?" +
                          "client_id=" + ClientIdentifier + "&" +
                          "redirect_uri=" + redirectUrl;
                HttpContext.Current.Response.Redirect(url);
                return FacebookAuthorisationResult.RequestingCode;
            }
            var token = ExchangeCodeForToken(verificationCode, redirectUrl);
            HttpContext.Current.Session[SESSION_NAME_TOKEN] = token;
            return FacebookAuthorisationResult.Authorized;
        }
        public Boolean IsCurrentUserAuthorized()
        {
            return HttpContext.Current.Session[SESSION_NAME_TOKEN] != null;
        }
        public FacebookGraph GetCurrentUser()
        {
            var token = HttpContext.Current.Session[SESSION_NAME_TOKEN];
            if (token == null)
                return null;
            var url = MeGraphEndpoint + "?" +
                      "access_token=" + token;
            var request = WebRequest.CreateDefault(new Uri(url));
            using (var response = request.GetResponse())
            {
                using (var responseStream = response.GetResponseStream())
                {
                    using (var responseReader = new StreamReader(responseStream))
                    {
                        var responseText = responseReader.ReadToEnd();
                        var user =  FacebookGraph.Deserialize(responseText);
                        return user;
                    }
                }
            }
        }
        private String ExchangeCodeForToken(String code, Uri redirectUrl)
        {
            var url = TokenEndpoint + "?" +
                      "client_id=" + ClientIdentifier + "&" +
                      "redirect_uri=" + redirectUrl + "&" +
                      "client_secret=" + Secret + "&" +
                      "code=" + code;
            var request = WebRequest.CreateDefault(new Uri(url));
            using (var response = request.GetResponse())
            {
                using (var responseStream = response.GetResponseStream())
                {
                    using (var responseReader = new StreamReader(responseStream))
                    {
                        var responseText = responseReader.ReadToEnd();
                        var token = responseText.Replace("access_token=", "");
                        return token;
                    }
                }
            }
        }
        private Uri GetResponseUrl(Uri url)
        {
            var urlAsString = url.ToString();
            var doesUrlContainQuestionMark = urlAsString.Contains("?");
            if (doesUrlContainQuestionMark)
            {
                // Remove any parameters. Apparently Facebook does not support state: http://forum.developers.facebook.net/viewtopic.php?pid=255231
                // If you do not do this, you will get 'Error validating verification code'
                urlAsString = urlAsString.Substring(0, urlAsString.IndexOf("?"));
            }
            var replaceLocalhostWithSubdomain = url.Host == "localhost";
            if (!replaceLocalhostWithSubdomain)
                return new Uri(urlAsString);
            // Facebook does not like localhost, you can only use the configured url. To get around this, log into facebook
            // and set your Site Domain setting, ie happycow.com. 
            // Next edit C:\Windows\System32\drivers\etc\hosts, adding the line: 
            // 127.0.0.1       local.happycow.cow
            // And lastly, set LocalSubDomain to local.happycow.cow
            urlAsString = urlAsString.Replace("localhost", LocalSubDomain);
            return new Uri(urlAsString);
        }
    }
    [DataContract]
    public class FacebookGraph
    {
        private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(FacebookGraph));
            // Note: Changed from int32 to string based on Antonin Jelinek advise of an overflow
        [DataMember(Name = "id")]
        public string Id { get; set; }

        [DataMember(Name = "name")]
        public string Name { get; set; }

        [DataMember(Name = "first_name")]
        public string FirstName { get; set; }

        [DataMember(Name = "last_name")]
        public string LastName { get; set; }

        [DataMember(Name = "link")]
        public Uri Link { get; set; }

        [DataMember(Name = "birthday")]
        public string Birthday { get; set; }

        public static FacebookGraph Deserialize(string json)
        {
            if (String.IsNullOrEmpty(json))
            {
                throw new ArgumentNullException("json");
            }

            return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json)));
        }

        public static FacebookGraph Deserialize(Stream jsonStream)
        {
            if (jsonStream == null)
            {
                throw new ArgumentNullException("jsonStream");
            }

            return (FacebookGraph)jsonSerializer.ReadObject(jsonStream);
        }
    }


}

Ответ 2

Решение Iain - это, наконец, то, с чем я работал.

Есть одна заметка для будущих исполнителей - похоже, что свойство идентификатора Facebook теперь превышает емкость типа Int32. Возможно, вам придется изменить это в классе FacebookGraph, я использовал простую строку.

Спасибо, Iain, ваш код действительно помог мне!

Ответ 3

Я обнаружил, что написать мою собственную реализацию было менее трудоемким, чем игра с DNOA. Это не очень сложно, хотя я действительно не проверил тщательную проверку кода; который, я думаю, будет основным предостережением.

Вероятно, это не так полезно, но я обнаружил, что только 1 раз в день, чтобы что-то работало.

Ответ 4

После долгой работы с обновлением DotNetOpenAuth и не испытывающего никакой удачи, подключающейся к Facebook, я тоже собрал код для поддержки входа в Facebook из моего приложения ASP.NET MVC.

Во-первых, код, например, должен находиться где-то в контроллере.

// You call this action to initiate the process with Facebook
public ActionResult FacebookLogIn()
{
    return CreateFacebookClient().RequestAuthorisation();
}

// Facebook will call you back here
public ActionResult FacebookAuthorisationResponse()
{
    var facebookClient = CreateFacebookClient();
    var authorisationResponse = facebookClient.HandleAuthorisationResponse();

    if (authorisationResponse.IsSuccess)
    {
        var accessToken = authorisationResponse.AccessToken;

        // TODO do whatever you want to do with your access token here

        return Redirect("SomeUrl");
    }

    // TODO handle the error somehow
    return Content(authorisationResponse.ErrorMessage);
}

private FacebookClient CreateFacebookClient()
{
    const string clientId = "xxxxxxxxxxxxxxx";
    const string appSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

    var redirectUrl = Url.Action("FacebookAuthorisationResponse", null, null, "http");

    return new FacebookClient(clientId, appSecret, redirectUrl);
}

Это почти все, что вам нужно сделать с вашим кодом. Когда у вас есть этот токен доступа, вы можете сделать следующее:

// Get basic information for this user
var basicInfoUrl = string.Format("https://graph.facebook.com/me?access_token={0}", Uri.EscapeDataString(accessToken.TokenString));
var json = new WebClient().DownloadString(basicInfoUrl);

Здесь приведен код, который поддерживает относительно простые вещи. Вы можете просто сбросить все это в файле в своем проекте:

// Drew Noakes, http://drewnoakes.com
// Created 08/08/2012 22:41

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;

namespace DrewNoakes.Facebook.Mvc
{
    public sealed class FacebookClient
    {
        private readonly string _clientId;
        private readonly string _appSecret;
        private readonly string _authorisationResponseUrl;

        public IFacebookClientStateManager StateManager { get; set; }

        public FacebookClient(string clientId, string appSecret, string authorisationResponseUrl)
        {
            _clientId = clientId;
            _appSecret = appSecret;
            _authorisationResponseUrl = authorisationResponseUrl;

            StateManager = MemoryStateManager.Instance;
        }

        public ActionResult RequestAuthorisation(string[] permissions = null)
        {
            // First step is to redirect the visitor browser to Facebook

            var state = StateManager.GetState();

            var url = string.Format("https://www.facebook.com/dialog/oauth?client_id={0}&redirect_uri={1}&scope={2}&state={3}",
                _clientId, Uri.EscapeDataString(_authorisationResponseUrl), permissions == null ? string.Empty : string.Join(",", permissions), state);

            return new RedirectResult(url, permanent: false);
        }

        public AuthorisationResponse HandleAuthorisationResponse()
        {
            var queryString = HttpContext.Current.Request.QueryString;

            // Ensure returned state is expected
            if (!StateManager.IsValidState(queryString["state"]))
                return AuthorisationResponse.Error("Invalid state");

            // TODO handle case where user declined: YOUR_REDIRECT_URI?error_reason=user_denied&error=access_denied&error_description=The+user+denied+your+request.&state=YOUR_STATE_VALUE

            var code = queryString["code"];
            var url = string.Format("https://graph.facebook.com/oauth/access_token?client_id={0}&redirect_uri={1}&code={3}&client_secret={2}",
                _clientId, Uri.EscapeDataString(_authorisationResponseUrl), _appSecret, Uri.EscapeDataString(code));

            var client = new WebClient { Proxy = null };
            var responseBody = client.DownloadString(url);

            // HTTP 200: access_token=USER_ACCESS_TOKEN&expires=NUMBER_OF_SECONDS_UNTIL_TOKEN_EXPIRES
            // HTTP 400: TODO handle JSON error reponse: { "error": { "type": "OAuthException", "message": "Error validating verification code." } }

            var response = HttpUtility.ParseQueryString(responseBody);
            var accessToken = response["access_token"];
            var expiresSecondsString = response["expires"];

            int expiresSeconds;
            if (!int.TryParse(expiresSecondsString, out expiresSeconds))
                return AuthorisationResponse.Error("Unable to parse expiration time");
            var expiresAtUtc = DateTime.UtcNow.AddSeconds(expiresSeconds);

            return AuthorisationResponse.Success(accessToken, expiresAtUtc);
        }
    }

    public class AuthorisationResponse
    {
        public bool IsSuccess { get; private set; }
        public AccessToken AccessToken { get; private set; }
        public string ErrorMessage { get; private set; }

        private AuthorisationResponse() { }

        public static AuthorisationResponse Error(string errorMessage)
        {
            return new AuthorisationResponse { IsSuccess = false, ErrorMessage = errorMessage };
        }

        public static AuthorisationResponse Success(string accessToken, DateTime expiresAtUtc)
        {
            return new AuthorisationResponse { IsSuccess = true, AccessToken = new AccessToken(accessToken, expiresAtUtc) };
        }
    }

    public struct AccessToken
    {
        public string TokenString { get; private set; }
        public DateTime ExpiresAtUtc { get; private set; }

        public AccessToken(string tokenString, DateTime expiresAtUtc)
            : this()
        {
            if (tokenString == null)
                throw new ArgumentNullException("tokenString");
            TokenString = tokenString;
            ExpiresAtUtc = expiresAtUtc;
        }
    }

    public interface IFacebookClientStateManager
    {
        string GetState();

        bool IsValidState(string state);
    }

    /// <summary>
    /// The default implementation of <see cref="IFacebookClientStateManager"/>.
    /// </summary>
    public sealed class MemoryStateManager : IFacebookClientStateManager
    {
        private static readonly IFacebookClientStateManager _instance = new MemoryStateManager();

        public static IFacebookClientStateManager Instance
        {
            get { return _instance; }
        }

        private readonly Dictionary<string, DateTime> _stateTimes = new Dictionary<string, DateTime>();

        public string GetState()
        {
            var state = Guid.NewGuid().ToString("N");
            _stateTimes[state] = DateTime.UtcNow;
            return state;
        }

        public bool IsValidState(string state)
        {
            var isValid = _stateTimes.Remove(state);

            // Remove any keys that have not been accessed within a given period
            var staleKeys = _stateTimes.Where(pair => pair.Value < DateTime.UtcNow.AddMinutes(-30)).Select(pair => pair.Key).ToList();

            foreach (var staleKey in staleKeys)
                _stateTimes.Remove(staleKey);

            return isValid;
        }
    }
}

Я собрал это вместе быстро сегодня вечером, но вернусь позже и исправлю его, если найду проблемы. Он работает очень хорошо на моем сайте прямо сейчас, хотя.

Существует несколько TODO, связанных с надежной обработкой ошибок.

Ответ 5

Я, с перерывами, испытывал эту же проблему сам, когда использовал параметр returnTo WebServerClient PrepareRequestUserAuthorization(). Только определенные URI-коды returnTo будут иметь проблему... URI, с которыми я проходил, имели для них компонент Base64. Некоторые из них содержали a = в них. Если я URL-адрес Кодирование этих URL-адресов, я получаю "потенциально опасное значение Request.Path было обнаружено из ошибки клиента (%)" с моего локального сервера.

До тех пор, пока я не смогу найти лучшее решение, я выполняю некоторую подтасовку в строке перед ее передачей;

localReturnTo = localReturnTo.Replace("=", "_")

Затем, когда я получаю свой ответ, я выполняю обратное:

returnedUri = returnedUri.Replace("_", "=")

Это некрасиво. Но он сталкивается с непосредственной (аналогичной) проблемой, которую я испытывал.