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

I am 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. I also need to add services for authentication and authorization, I have added one authorization scheme for administrators.

using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Annytab.Middleware;
using Annytab.Repositories;
using Annytab.Options;
using Annytab.Rules;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;

namespace Hockeytabeller
{
    /// <summary>
    /// This class handles application startup
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// Variables
        /// </summary>
        public IConfiguration configuration { get; set; }

        /// <summary>
        /// Create a new startup object
        /// </summary>
        /// <param name="configuration">A reference to configuration</param>
        public Startup(IConfiguration configuration)
        {
            this.configuration = configuration;

        } // End of the constructor method

        /// <summary>
        /// This method is used to add services to the container.
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            // Add the mvc framework
            services.AddRazorPages();

            // Add memory cache
            services.AddDistributedMemoryCache();

            // Add redis distributed cache
            if (configuration.GetSection("AppSettings")["RedisConnectionString"] != "")
            {
                services.AddDistributedRedisCache(options =>
                {
                    options.Configuration = configuration.GetSection("AppSettings")["RedisConnectionString"];
                    options.InstanceName = "Hockeytabeller:";
                });
            }

            // Add cors
            services.AddCors(options =>
            {
                options.AddPolicy("AnyOrigin", builder => builder.AllowAnyOrigin());
            });

            // Add the session service
            services.AddSession(options =>
            {
                // Set session options
                options.IdleTimeout = TimeSpan.FromMinutes(20d);
                options.Cookie.Name = ".Hockeytabeller";
                options.Cookie.Path = "/";
                options.Cookie.HttpOnly = true;
                options.Cookie.SameSite = SameSiteMode.Lax;
                options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            });

            // Create database options
            services.Configure<DatabaseOptions>(options => 
            {
                options.connection_string = configuration.GetSection("AppSettings")["ConnectionString"];
                options.sql_retry_count = 1;
            });

            // Create cache options
            services.Configure<CacheOptions>(options => 
            {
                options.expiration_in_minutes = 240d;
            });

            // Add Authentication
            services.AddAuthentication()
                .AddCookie("Administrator", options =>
                {
                    options.ExpireTimeSpan = TimeSpan.FromDays(1);
                    options.Cookie.MaxAge = TimeSpan.FromDays(1);
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Events.OnRedirectToLogin = (context) =>
                    {
                        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                        context.Response.Redirect("/admin_login");
                        return Task.CompletedTask;
                    };
                })
                .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("ApiAuthentication", null);


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

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

            // Add repositories
            services.AddSingleton<IDatabaseRepository, MsSqlRepository>();
            services.AddSingleton<IWebsiteSettingRepository, WebsiteSettingRepository>();
            services.AddSingleton<IAdministratorRepository, AdministratorRepository>();
            services.AddSingleton<IFinalRepository, FinalRepository>();
            services.AddSingleton<IGroupRepository, GroupRepository>();
            services.AddSingleton<IStaticPageRepository, StaticPageRepository>();
            services.AddSingleton<IXslTemplateRepository, XslTemplateRepository>();
            services.AddSingleton<ISitemapRepository, SitemapRepository>();
            services.AddSingleton<IXslProcessorRepository, XslProcessorRepository>();
            services.AddSingleton<ICommonServices, CommonServices>();

        } // End of the ConfigureServices method

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IWebsiteSettingRepository website_settings_repository)
        {
            // Use error handling
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseStatusCodePagesWithReExecute("/home/error/{0}");
            }

            // Get website settings
            KeyStringList settings = website_settings_repository.GetAllFromCache();
            bool redirect_https = settings.Get("REDIRECT-HTTPS") == "true" ? true : false;
            bool redirect_www = settings.Get("REDIRECT-WWW") == "true" ? true : false;
            bool redirect_non_www = settings.Get("REDIRECT-NON-WWW") == "true" ? true : false;

            // Add redirection and use a rewriter
            RedirectHttpsWwwNonWwwRule rule = new RedirectHttpsWwwNonWwwRule
            {
                status_code = 301,
                redirect_to_https = redirect_https,
                redirect_to_www = redirect_www,
                redirect_to_non_www = redirect_non_www,
                hosts_to_ignore = new string[] { "localhost", "hockeytabeller001.azurewebsites.net" }
            };
            RewriteOptions options = new RewriteOptions();
            options.Rules.Add(rule);
            app.UseRewriter(options);

            // Use static files
            app.UseStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                {
                    // Cache static files for 30 days
                    ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=25920000");
                    ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(300).ToString("R", CultureInfo.InvariantCulture));
                }
            });

            // Use sessions
            app.UseSession();

            // For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must 
            // appear between the calls to UseRouting and UseEndpoints to be effective.
            app.UseRouting();

            // Use CORS
            app.UseCors();

            // Use authentication and authorization middlewares
            app.UseAuthentication();
            app.UseAuthorization();

            // Routing endpoints
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    "default",
                    "{controller=home}/{action=index}/{id?}");
            });

        } // End of the Configure method

    } // End of the class

} // End of the namespace

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#.

using System;

namespace Annytab.Models
{
    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

} // End of the namespace

Repository

I have created an administrator class that contains methods concerning administrators and I have created the following methods to get a facebook user. This repository injects IHttpClientFactory as client_factory, you need to modify FACEBOOK_APP_ID and FACEBOOK_APP_SECRET.

/// <summary>
/// Get a facebook user
/// </summary>
public async Task<FacebookUser> GetFacebookUser(string code)
{
    // Create variables
    FacebookAuthorization facebook_authorization = null;
    FacebookUser facebook_user = new 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=" + "FACEBOOK_APP_ID" +
    "&redirect_uri=" + "http://localhost:59448" + "/facebook/login_callback" + "&client_secret="
    + "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 = JsonSerializer.Deserialize<FacebookAuthorization>(await
        response.Content.ReadAsStringAsync());
    }
    else
    {
        // Get an error
        FacebookErrorRoot root = JsonSerializer.Deserialize<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 = JsonSerializer.Deserialize<FacebookUser>(await response.Content.ReadAsStringAsync());
    }
    else
    {
        // Get an error
        FacebookErrorRoot root = JsonSerializer.Deserialize<FacebookErrorRoot>(await response.Content.ReadAsStringAsync());
    }

    // Return the facebook user
    return facebook_user;

} // End of the GetFacebookUser method

/// <summary>
/// Get one administrator based on facebook user id
/// </summary>
public Administrator GetByFacebookUserId(string facebook_user_id)
{
    // Create the sql statement
    string sql = "SELECT * FROM dbo.administrators WHERE facebook_user_id = @facebook_user_id;";

    // Create parameters
    IDictionary<string, object> parameters = new Dictionary<string, object>();
    parameters.Add("@facebook_user_id", facebook_user_id);

    // Get the post
    Administrator post = this.database_repository.GetModel<Administrator>(sql, parameters);

    // Return the post
    return post;

} // End of the GetByFacebookUserId method

Controller

Our facebook controller includes two methods to handle facebook login. I need to use an AuthenticationScheme to be able to get user context data, methods is marked with AllowAnonymous to allow access from not authenticated users.

using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Annytab.Repositories;
using Annytab.Models;
using Annytab.Helpers;

namespace Hockeytabeller.Controllers
{
    /// <summary>
    /// This class handles facebook login
    /// </summary>
    [Authorize(AuthenticationSchemes = "Administrator")]
    public class facebookController : Controller
    {
        #region Variables

        private readonly IDataProtector data_protector;
        private readonly IAdministratorRepository administrator_repository;

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new controller
        /// </summary>
        public facebookController(IDataProtectionProvider provider, IAdministratorRepository administrator_repository)
        {
            // Set values for instance variables
            this.data_protector = provider.CreateProtector("AuthTicket");
            this.administrator_repository = administrator_repository;

        } // End of the constructor

        #endregion

        #region Post methods

        // Redirect the user to the facebook login
        // GET: /facebook/login
        [HttpGet]
        [AllowAnonymous]
        public IActionResult login()
        {
            // Create a random state
            string state = DataValidation.GeneratePassword();

            // Add session variables
            ControllerContext.HttpContext.Session.Set<string>("FacebookState", state);
            ControllerContext.HttpContext.Session.Set<string>("FacebookReturnUrl", "/admin_default");

            // Create the url
            string url = "https://www.facebook.com/dialog/oauth?client_id=" + "FACEBOOK_APP_ID" + "&state=" +
            state + "&response_type=code&redirect_uri=" + "http://localhost:59448" + "/facebook/login_callback";

            // Redirect the user
            return Redirect(url);

        } // End of the login method

        // Login the user with facebook
        // GET: /facebook/login_callback
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> login_callback()
        {
            // 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
            FacebookUser facebook_user = await this.administrator_repository.GetFacebookUser(code);

            // Get the signed in user
            Administrator user = this.administrator_repository.GetOneByUsername(HttpContext.User.Identity.Name);

            // Check if the user exists or not
            if (facebook_user != null && user != null)
            {
                // Update the user
                user.facebook_user_id = facebook_user.id;
                this.administrator_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
                user = this.administrator_repository.GetByFacebookUserId(facebook_user.id);

                // Check if the user exists
                if (user == null)
                {
                    // Create a new user
                    user = new Administrator();
                    user.admin_user_name = facebook_user.id + "@" + DataValidation.GeneratePassword();
                    user.facebook_user_id = facebook_user.id;
                    user.admin_password = PasswordHash.CreateHash(DataValidation.GeneratePassword());
                    user.admin_role = "Editor";

                    // Add a user
                    int id = this.administrator_repository.Add(user);
                    this.administrator_repository.UpdatePassword(id, user.admin_password);
                }

                // Create claims
                ClaimsIdentity identity = new ClaimsIdentity("Administrator");
                //identity.AddClaim(new Claim("administrator", JsonSerializer.Serialize(user)));
                identity.AddClaim(new Claim(ClaimTypes.Name, user.admin_user_name));
                identity.AddClaim(new Claim(ClaimTypes.Role, user.admin_role));
                ClaimsPrincipal principal = new ClaimsPrincipal(identity);

                // Sign in the administrator
                await HttpContext.SignInAsync("Administrator", principal);

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

        } // End of the login_callback method

        // Sign out the user
        // GET: /facebook/logout
        [HttpGet]
        public async Task<IActionResult> logout()
        {
            // Sign out the administrator
            await HttpContext.SignOutAsync("Administrator");

            // Redirect the user to the login page
            return RedirectToAction("index", "admin_login");

        } // End of the logout method

        #endregion

    } // End of the class

} // End of the namespace

Leave a Reply

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