Login with Facebook in ASP.NET Core

This post describes how you can add Facebook login to your website in ASP.NET Core. You need to create an Facebook App and add Facebook Login as a product. You will need the App ID and the App Secret of your Facebook App to be able to connect to Facebooks Api, you also need to make sure that you add your callback url to Valid OAuth Redirect URIs in the settings for Facebook Login.

When the user first signs in with its Facebook account or when the user connects its account to Facebook, you need to save the Facebook Id of the user to your database. A Facebook Id is a long (Int64) but it can preferable be saved as a string. The Facebook Id will be used to find the user when he signs in for the second time.

Services

We are going to need a HttpClient and a Session service to be able to implement Facebook login, we add these services in the ConfigureServices method in the StartUp class of our project. It is not good practice to create and dispose of HttpClients in the application, you should use static HttpClients or the IHttpClientFactory. We use the IHttpClientFactory to add a named client.

// Add memory cache
services.AddDistributedMemoryCache();

// Add a session service
services.AddSession(options =>
{
    // Set session options
    options.IdleTimeout = TimeSpan.FromMinutes(30d);
    options.Cookie.Name = ".Mysite";
    options.Cookie.Path = "/";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

// Add clients
services.AddHttpClient("default", client =>
{
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });

We also need to use sessions in the Configure method in the StartUp class.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Use sessions
    app.UseSession();

    // More code ...

} // End of the Configure method

Models

Responses from the Facebook Api will be in JSON and we will create some models to be able to work with the response data in a convenient way. These models have been created based on pure JSON responses from the Facebook Api. It is easy to serialize a model to JSON and it is easy to deserialize JSON to a model in C#.

public class FacebookAuthorization
{
    #region Variables

    public string access_token { get; set; }
    public string token_type { get; set; }

    #endregion

    #region Constructors

    public FacebookAuthorization()
    {
        // Set values for instance variables
        this.access_token = null;
        this.token_type = null;

    } // End of the constructor

    #endregion

} // End of the class

public class FacebookErrorRoot
{
    #region Variables

    public FacebookError error { get; set; }

    #endregion

    #region Constructors

    public FacebookErrorRoot()
    {
        // Set values for instance variables
        this.error = null;

    } // End of the constructor

    #endregion

} // End of the class

public class FacebookError
{
    #region Variables

    public string message { get; set; }
    public string type { get; set; }
    public Int32? code { get; set; }
    public Int32? error_subcode { get; set; }
    public string fbtrace_id { get; set; }

    #endregion

    #region Constructors

    public FacebookError()
    {
        this.message = null;
        this.type = null;
        this.code = null;
        this.error_subcode = null;
        this.fbtrace_id = null;

    } // End of the constructor

    #endregion

} // End of the class

public class FacebookUser
{
    #region Variables

    public string id { get; set; }
    public string name { get; set; }

    #endregion

    #region Constructors

    public FacebookUser()
    {
        // Set values for instance variables
        this.id = null;
        this.name = null;

    } // End of the constructor

    #endregion

} // End of the class

Repository

We have created a user class that contains methods concerning users and we have created the following method to get a facebook user. This repository injects IHttpClientFactory as client_factory and the current_domain parameter includes the Facebook App Id and the Facebook App Secret.

/// <summary>
/// Get a facebook user
/// </summary>
public async Task<ModelItem<FacebookUser>> GetFacebookUser(WebDomain current_domain, string code)
{
    // Create variables
    FacebookAuthorization facebook_authorization = null;
    ModelItem<FacebookUser> facebook_user = new ModelItem<FacebookUser>();

    // Get a http client
    HttpClient client = this.client_factory.CreateClient("default");

    // Create the url
    string url = "https://graph.facebook.com/oauth/access_token?client_id=" + current_domain.facebook_app_id + 
    "&redirect_uri=" + current_domain.web_address + "/auth/facebook_login_callback" + "&client_secret=" 
    + current_domain.facebook_app_secret + "&code=" + code;

    // Get the response
    HttpResponseMessage response = await client.GetAsync(url);

    // Make sure that the response is successful
    if (response.IsSuccessStatusCode)
    {
        // Get facebook authorization
        facebook_authorization = JsonConvert.DeserializeObject<FacebookAuthorization>(await 
        response.Content.ReadAsStringAsync());
    }
    else
    {
        // Get an error
        FacebookErrorRoot root = JsonConvert.DeserializeObject<FacebookErrorRoot>(await 
        response.Content.ReadAsStringAsync());
    }

    // Make sure that facebook authorization not is null
    if(facebook_authorization == null)
    {
        return null;
    }

    // Modify the url
    url = "https://graph.facebook.com/me?fields=id,name&access_token=" + facebook_authorization.access_token;

    // Get the response
    response = await client.GetAsync(url);

    // Make sure that the response is successful
    if (response.IsSuccessStatusCode)
    {
        // Get a facebook user
        facebook_user.item = JsonConvert.DeserializeObject<FacebookUser>(await 
        response.Content.ReadAsStringAsync());
    }
    else
    {
        // Get an error
        FacebookErrorRoot root = JsonConvert.DeserializeObject<FacebookErrorRoot>(await 
        response.Content.ReadAsStringAsync());
    }

    // Return the facebook user
    return facebook_user;

} // End of the GetFacebookUser method

Controller

Our authorization controller includes 2 methods to handle facebook login. We have injected IDataProtectionProvider provider and our user class IUserRepository user_repository in to this controller. A IDataProtector data_protector is created from the provider.

// Redirect the user to the facebook login
// GET: /auth/facebook_login?return_url=
[HttpGet]
public async Task<IActionResult> facebook_login(string return_url)
{
    // Get the current domain
    WebDomain current_domain = await this.web_domain_repository.GetCurrentDomain(ControllerContext.HttpContext);

    // Create a random state
    string state = Tools.GeneratePassword();
    ControllerContext.HttpContext.Session.Set<string>("FacebookState", state);
    ControllerContext.HttpContext.Session.Set<string>("FacebookReturnUrl", return_url);

    // Create the url
    string url = "https://www.facebook.com/dialog/oauth?client_id=" + current_domain.facebook_app_id + "&state=" + 
    state + "&response_type=code&redirect_uri=" + current_domain.web_address + "/auth/facebook_login_callback";

    // Redirect the user
    return Redirect(url);

} // End of the facebook_login method

// Login the user with facebook
// GET: /auth/facebook_login_callback
[HttpGet]
public async Task<IActionResult> facebook_login_callback()
{
    // Get the current domain
    WebDomain current_domain = await this.web_domain_repository.GetCurrentDomain(ControllerContext.HttpContext);

    // Get the state
    string state = "";
    if (ControllerContext.HttpContext.Request.Query.ContainsKey("state") == true)
    {
        state = ControllerContext.HttpContext.Request.Query["state"].ToString();
    }

    // Get sessions
    string session_state = ControllerContext.HttpContext.Session.Get<string>("FacebookState");
    string return_url = ControllerContext.HttpContext.Session.Get<string>("FacebookReturnUrl");

    // Get the code
    string code = "";
    if (ControllerContext.HttpContext.Request.Query.ContainsKey("code") == true)
    {
        code = ControllerContext.HttpContext.Request.Query["code"];
    }

    // Make sure that the callback is valid
    if (state != session_state || code == "")
    {
        return Redirect("/");
    }

    // Get a facebook user
    ModelItem<FacebookUser> facebook_user_model = await this.user_repository.GetFacebookUser(current_domain, code);
    FacebookUser facebook_user = facebook_user_model.item;

    // Get the signed in user
    Claim claim = ControllerContext.HttpContext.User.FindFirst("user");
    UserDocument user = claim != null ? JsonConvert.DeserializeObject<UserDocument>(claim.Value) : null;

    // Check if the user exists or not
    if (facebook_user != null && user != null)
    {
        // Update the user
        user.facebook_user_id = facebook_user.id;
        await this.user_repository.Update(user);

        // Redirect the user to the return url
        return Redirect(return_url);
    }
    else if (facebook_user != null && user == null)
    {
        // Check if we can find a user with the facebook id
        ModelItem<UserDocument> user_model = await this.user_repository.GetByFacebookUserId(facebook_user.id);
        user = user_model.item;

        // Check if the user exists
        if (user == null)
        {
            // Create a new user
            user = new UserDocument();
            user.facebook_user_id = facebook_user.id;
            user.public_name = facebook_user.name;
            user.user_email = user.id + "@myfavorite.se";
            user.user_password = PasswordHash.CreateHash(Tools.GeneratePassword());
            user.user_role = "User";

            // Add a user
            await this.user_repository.Upsert(user);

            // Create a cookie
            CookieOptions options = new CookieOptions();
            options.Expires = DateTime.UtcNow.AddDays(30);
            options.HttpOnly = true;
            options.SameSite = SameSiteMode.Lax;
            ControllerContext.HttpContext.Response.Cookies.Append("User", this.data_protector.Protect(user.id), 
            options);

            // Redirect the user to the edit user page
            return Redirect(return_url);
        }
        else
        {
            // Create a cookie
            CookieOptions options = new CookieOptions();
            options.Expires = DateTime.UtcNow.AddDays(30);
            options.HttpOnly = true;
            options.SameSite = SameSiteMode.Lax;
            ControllerContext.HttpContext.Response.Cookies.Append("User", this.data_protector.Protect(user.id), 
            options);

            // Redirect the user to the start page
            return Redirect(return_url);
        }
    }
    else
    {
        // Redirect the user to the login
        return Redirect("/auth/login");
    }

} // End of the facebook_login_callback method

Leave a Reply

Your email address will not be published. Required fields are marked *