I am trying to use the CTP to connect with Facebook over OAuth 2.0.
I can get the initial request to Facebook working OK, but when it comes back and we call:
<Iain's solution is finally something I made this thing work with.
There is one note for future implementers - seems like Facebook ID property is now exceeding capacity of Int32 type. You might need to change this in FacebookGraph class, I used plain string.
Thanks Iain, your code really helped me!
I found that writing my own implementation was less time-consuming than playing with DNOA. It's not very difficult, although I haven't really done a thorough security check of the code; which i guess would be a major caveat.
That's probably not that helpful, but I found it only took 1/2 a day to get something working.
After messing around with an upgrade of DotNetOpenAuth for a long while and not experiencing any luck connecting to Facebook, I too put together some code to support Facebook login from within my ASP.NET MVC app.
Firstly, code such as this should go in a controller somewhere.
// 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);
}
That's pretty much all you need to do with your code. Once you have that access token, you can do things like this:
// 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);
The code that supports the relatively simple stuff above is here. You can just dump all of this in a file in your project:
// 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's 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;
}
}
}
I threw this together quickly tonight, but will come back later and patch it if I find issues. It's working really well on my site right now though.
There are a couple of TODOs related to robust error response handling.
After hitting this issue, I wrote my own code to authorize, and get the users details. Another approach would be to use Facebook C# SDK. As a starter for anyone else thinking about doing there own, here is how I did it. Please note I have not looked into error cases.
Firstly, read facebooks doc on how it works (its rather simple!)
I consume it like this:
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("/");
}
And the client code:
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);
}
}
}
I've been, intermittently, experiencing this same issue myself when I was utilising the returnTo
parameter of WebServerClient
's PrepareRequestUserAuthorization()
. Only certain returnTo
URIs would experience an issue... the URIs I was passing in had a Base64 component to them. Some of which contained a = in them. If I URL Encode these URLs I get "A potentially dangerous Request.Path value was detected from the client (%)" error from my local server.
Until I can find a, better, solution I am performing some prodding on the string before passing it in;
localReturnTo = localReturnTo.Replace("=", "_")
Then when I receive my response I perform the reverse;
returnedUri = returnedUri.Replace("_", "=")
It's not pretty. But it does get around the immediate (similar) problem I was experiencing.