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