Inloggning med Facebook i ASP.NET Core

I det här inlägget beskrivs hur du kan lägga till Facebook-inloggning på din webbplats i ASP.NET Core. Du måste skapa en Facebook Applikation och lägga till Facebook Login som en produkt. Du behöver App ID och App Secret avseende din Facebook App för att kunna använda api:et, du måste också se till att du lägger till din callback url till Valid OAuth Redirect URI i inställningarna för Facebook Login.

När användaren loggar in med sitt Facebook-konto för första gången eller när användaren ansluter sitt konto till Facebook måste du spara användarens Facebook-ID i din databas. Ett Facebook-ID är ett långt heltal (Int64) men kan med fördel sparas som en sträng. Facebook-ID kommer att användas för att hitta användaren när han loggar in för andra gången.

Tjänster

Vi kommer att behöva en HttpClient och en sessionstjänst för att kunna implementera Facebook-inloggning, vi lägger till dessa tjänster i metoden ConfigureServices i klassen StartUp i vårt projekt. Det är inte bra att löpande skapa och kassera HttpClients i ett program, man bör använda statiska HttpClients eller IHttpClientFactory. Vi använder IHttpClientFactory för att skapa en namngiven klient. Jag måste också lägga till tjänster för autentisering och auktorisation, jag har lagt till ett auktorisationsschema för administratörer.

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

Modeller

Svar från anrop till Facebook Api:et kommer som JSON och vi har skapat modeller för att kunna arbeta med svarsdatan på ett bekvämt sätt. Dessa modeller har skapats baserat på rena JSON-svar från Facebook Api:et. Det är enkelt att serialisera en modell till JSON och det är enkelt att deserialisera JSON till en modell i 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

Klass för användare

Jag har skapat en klass som innehåller metoder avseende administratörer och jag har skapat följande metoder för att hämta en Facebook-användare. Denna klass injicerar IHttpClientFactory som client_factory, du måste modifiera FACEBOOK_APP_ID och 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

Vår facebook-controller innehåller två metoder för hantering av Facebook-inloggning. Jag måste använda ett AuthenticationScheme för att kunna få användarkontextdata, metoderna är markerade med AllowAnonymous för att tillåta åtkomst från icke autentiserade användare.

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

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *