Feb 2020. This tutorial is a hands-on demo how to implement Local Login and Social Login like Google, Facebook & Twitter in ASP.NET Core 3.
Instead of the usual static login page tutorials, we will implement a dynamic login dialog like the one below, featuring branding, both social & local login together with account registering, email confirmation and "forgot password" logic - it's a full blown website login tutorial.
Today many (if not most) websites allows you to both register & login by just clicking a button from a social login provider, say a Facebook button, and accept the consent screen from that provider and you are logged on.
Many website owners reports increased conversion then adding a social login option and that is not strange since it makes registering with a website a lot easier for the user - you don't need to remember to remember any email nor password and you don't need to write anything, you only need to click 2 buttons first time and subsequent logins only require a single click.
Prepare the Social Login Providers
Each social login provider will need the following
domains on which login is allowed
clientId (public) & clientSecret (not public)
a consent screen
Google : url : if you need me to hold you hand see appendix :
Facebook : url : if you need me to hold you hand see appendix :
Twitter : url : if you need me to hold your hand see appendix :
Setting up the Visual Studio project
If you have not created your Visual Studio project yet, here is a fast walk through how to do that
Open Visual Studio 2019
Create a new Empty project (i think the Visual Studio templates are good to learn from, but I prefer to start a new project on an empty template)
Create a new project
Select ASP.NET Core Web Application
Configure your new project
Make it an Empty project
Project created !
In Visual Studio press ctrl+F5 to confirm you can build the new project - you should see a browser tab opened with the text Hello World.
Create relevant folders (under the project node, in my case Tooling)
wwwroot : not in use here, I just like to have it
Controllers
Data
Entities
Migrations
Repositories
Models
Views
Home
Shared
Create initial MVC project
Create the _Layout View
Create the _ViewStart View
Create the HomeController
Create the Home Index View
Open Startup to add MVC structure
Press ctrl+F5 - you should now see your own Index file instead of the default Hello World
Adding Email Support
For development we can use our GMail account (if you don't have one, go to Google and create one). GMail allows us to connect remotely to their SMTP server and send 50 emails per day - enough for development.
Ok, with a minimal MVC structure established, we should get started on the login data structure. There are 2 types of login :
Local Login : standard email & password box with no social api involved
Social Login : we login through social media
It would give meaning to start creating the Local login first and then add the Social login later, and for the logic that is what we will do, however for the data structure it is more easy to start with the social login right away so that we don't have to change the Entity model later.
Creating the context class
Rather than manually creating all the Entities supporting social login, ASP.NET Core (like the Framework before) comes with a nifty class, IdentityDbContext, that contains all social login relevant Entities.
Install necessary NuGet packages (2 packages)
Microsoft.AspNetCore.Identity.EntityFrameworkCore : contains the IdentityDbContext class.
In Solution Explorer right click on "Dependencies" and select "Manage NuGet Packages" to get to the NuGet installer dialog.
Be sure the "Browse" tab is active and search for Microsoft.AspNetCore.Identit - a lot of packages in the search result, select the .EntityFrameworkCore package and click on the "Install" button.
In Solution Explorer expand "Dependencies" | "Packages" and confirm that the .EntityFrameworkCore package have been installed.
Microsoft.EntityFrameworkCore.SqlServer : adds the UseSqlServer extension function to the DbContextOptionsBuilder class. We need UseSqlServer since we are going to use SqlServer (that is : the free MSSQLLocalDB version of SqlServer, which is the default version for Visual Studio development)
Again right click on "Depenencies" and select "Manage NuGet Packages".
Search for Microsoft.EntityFrameworkCore - it should be the first package, select it and click on the "Install" button.
Back in Solution Explorer expand "Dependencies" | "Packages" to confirm that the .SqlServer package have been installed.
Write the Context class
In Solution Explorer right click on the "Data" folder to create a new class file called ToolingContext
In the "Add New Item" dialog select "Class" and name it <YourProjectName>Context (in my case ToolingContext). Click the "Add" button.
Rewrite the ToolingContext.cs file to this :
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Tooling.Data
{
public class ToolingContext : IdentityDbContext
{
public ToolingContext(DbContextOptions<ToolingContext> options) : base(options) { }
}
}
By inheriting from IdentityDbContext (instead of DbContext directly), we will get all the social login Entities created automatically (otherwise we would need to add them ourself in the ToolingContext class like eg. public DbSet<AspNetUser> AspNetUsers;).
Add the Context class (in my case ToolingContext) to the Dependency Injection Container (DIC)
Open the Startup.cs file
Rewrite the Startup template code to the following : (5 small additions)
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore; // Added (1) using Microsoft.Extensions.Configuration; // Added (2)
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services)
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Apart from the using statements, there are 2 important code additions :
Configuration (4) : since version 3.0 the Configuration object (default in DIC already by the ASP.NET Core framework) automatically add all appsettings.{Environment}.json files, so we only need to declare & initialize a variable to hold that Configuration object.
Context added to DIC (5) : here we use the AddDbContext method on the IServiceCollection to add our Context object to DIC. AddDbContext takes a DbContextOptionsBuilder instance on which we use the UseSqlServer extension function partly to specify what database to use and partly to add a connection string.
Create the database
With the Context object (in my case ToolingContext) created and added to DIC, we are ready to create the database and the social login related tables (defined in IdentityDbContext).
Creating the database from the Context is a 2 step process :
Define a connection string in appsettings.{Environment}.json (
appsettings.json : for production (we will NOT use this here, instead leaving it up to your to create a connection string for your production scenario.
appsettings.Development.json : for development (we will define our connection string here)
Call up our context object to create the database (using the connection string defined in appsettings.Development.json)
In Solution Explorer expand appsettings.json to make appsettings.Development.json visible (if File nesting is enabled, default, appsettings.Development.json will nest under appsettings.json) and double click on appsettings.Development.json to open it.
Change appsettings.Development.json to : (added code is in bold)
Under "ConnectionStrings" add another key for a particular connection string - this allows multiple connection strings and the first is often called "default", however I like to use the name of the database to fast see which database I connect to - so in my case I call this subkey "Tooling".
The subkey (in my case "Tooling") have the first following connection string : "Server=(localdb)\\mssqllocaldb;Database=Tooling;Trusted_Connection=True;MultipleActiveResultSets=True"
Server : we specify mssqllocaldb (for development) since that comes automatic with Visual Studio.
Database : the name of the database to create - in my case Tooling.
Trusted_Connection : same as Integrated Security=SSPI (exact wording for Windows authentication depends on provider) : if set to true allowing Windows authentication instead of passing credentials via connection string.
MultipleActiveResultSets : if set to True allows you to read from a SqlDataReader and execute additional batches (eg. inserts) without having two connections open.
Open the Program.cs file and rewrite it to the following :
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Tooling.Data;
namespace Tooling
{
public class Program
{
public static void Main(string[] args)
{
//CreateHostBuilder(args).Build().Run(); // original from template var host = CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
public static void CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ToolingContext>();
context.Database.EnsureCreated(); // Here }
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the database.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Program.Main is the entry point for starting up the ASP.NET Core App Host - after the app host is build, we intercept before running it to create the datastore (if it does not exist) in CreateDbIfNotExists.
In CreateDbIfNotExists we get our context object (in my case ToolingContext) and use it to create the database if it does not exist using context.Database.EnsureCreated(); (this should not be added to any database seeding class anymore).
Press F5 (not ctrl+F5) to build your project & execute the app host and load the project url in a browser (using only F5 we are guaranteed to run Program.Main and therefore execute EnsureCreated) - this will create the database (as specified in our connection string) and add the social login related tables (as defined by Microsoft in IdentityDbContext).
Check that the database have been properly created
In case SQL Server Object Explorer is not visible select View from the main menu and then SQL Server Object Explorer in the dropdown.
SQL Server Object Explorer should now be visible. Expand the MSSQLLocalDB node.
You should be able to see that your database have been created by the name (in my case Tooling) specified in your connection string (in appsettings.Development.json).
Expand your database and then expand "Tables" - you should be able to see the 7 social login related tables (specified by Microsoft in IdentityDbContext). CONGRATULATION - you have created the data structure for social login.
_EFMigrationsHistory : not a social login table, instead this table holds migration history - each time you execute a migration, the name of that migration will be added to this table.
AspNetRoleClaims :
AspNetRols :
AspNetUserClaims :
AspNetUserLogins :
AspNetUserRoles :
AspNetUsers : we will add a custom property, ProfilePictureUrl, to this table.
AspNetUserTokens :
Syncronize database & Entity model
After we have created the database, we better syncronize it with the Entity model. The problem right now is that if we add a migration, that migration will create table statements for the social login tables that already exists in the database - resulting in an error making it impossible to change the database further.
Install the Microsoft.EntityFrameworkCore.Tools NuGet package - this will allow you to execute migration commands from the Package Manager Console :
In Solution Explorer right click on "Dependencies" and select "Manage NuGet Packages".
Search for Microsoft.EntityFrameworkCore.Tools, select it and click "Install".
Check that the NuGet package have been installed.
Open the Package Manager Console
Package Manager Console is open
Create an empty migration to syncronize migrations with the database
PM> add-migrations Initial -o Data\Migrations :
Your migration code file will be built and opened in a tab (2a) and you can see the migration code file in Solution Explorer (2b). A ContextModelSnapshot file (2c) (in my case ToolingContextModelSnapshot) have also been created (which happens first time a migration is added), which contains the state of the database relative to the Entity model.
Note that we are using EF Core which does not yet have a good way to handle an existing database (in EF6 we would have done like this : PM> add-migration Initial -IgnoreContext, and we would have been finished, however in EF Core we need to go through the hoop below)
The "Initial" migration code file contains 2 methods, Up & Down, containing statements for creating the social login tables from IdentityDbContext which are already in the table - this will result in an error if executed. Therefore out-comment both methods (Up & Down) so you have an empty class - we want to execute nothing and add that execution to our ContextModelSnapshot (which happens automatically on update-database), so that next time we change the Entity model, only those changes will be pushed to the database.
PM> update-database : update the database with the empty migration code file will have no effect on the database schema but will update ContextModelSnapshot as well as the _EFMigrationsHistory table in the database - your database and Entity model is now syncronized.
Custom User Properties
While the social login data structure above (the 7 tables) is built by IdentityDbContext, the AspNetUser table is actually created based on the builtin IdentityUser class. If we want more properties on the User than the few generic builtin properties in IdentityUser, we need to extend the IdentityUser class and specify that we want to use our own custom version of IdentityUser instead of the generic class.
By convention a custom IdentityUser class is named after your application name and then postfixed the word User - in my case it will be ToolingUser.
Creating the custom user class (my my case ToolingUser)
In Visual Studio Solution Explorer right click on the "Data" folder and add a new class file named ToolingUser (your name will be different)
Change the ToolingUser class to inherit from IdentityUser and add the ProfilePictureUrl property :
using Microsoft.AspNetCore.Identity;
namespace Tooling.Data
{
public class ToolingUser : IdentityUser
{
public string ProfilePictureUrl { get; set; }
}
}
Specify that we want IdentityDbContext to use our custom IdentityUser class ToolingUser
In Solution Explorer open your Context class file (in my case ToolingContext)
Specify that ToolingContext should extend IdentityDbContext<ToolingUser> (not the generic IdentityDbContext) :
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Tooling.Data
{
public class ToolingContext : IdentityDbContext<ToolingUser> // changed {
public ToolingContext(DbContextOptions<ToolingContext> options) : base(options) { }
}
}
Add a migration to update the database
Open Package Manager Console
PM> add migrations User_ProfilePictureUrl -o Data\Migrations : create a migration for any Entity changes.
You migration code file will be built and opened in a tab (2a) and you can also see the migration code file in Solution Explorer (2b)
Take a closer look at the migration file, you can see the Up-function will add a ProfilePictureUrl column to the AspNetUsers table :
PM> update-database : execute new migrations on the database
Check the database to see that ProfilePictureUrl have been added AspNetUser.
In Sql Server Explorer expand your Tables node under your database and right click on AspNetUser and select "View Designer" in the shortcut menu.
AspNetUsers Designer is open and you can see the ProfilePictureUrl column have been added.
Create a local login logic
Not everybody want to login with a social profile, so we also want to supply the standard local login with an email and password box.
Let's not build the usual clean model based local login that you can lookup many places or see in the Visual Studio Authentication template, instead let's create a fast ugly but efficient local login page (that you would never build in real life) but absolutely works and well illustrates the local login process and how local login works together with social login.
Create the _Layout view
Create the _ViewStart view
Create the social login logic
Install NuGet packages for the external login providers
In your Visual Studio project right click on "Dependencies" in Solution Explorer.
Select "Manage NuGet Packages" from the Dependencies dropdown.
On the "Nuget Package Manager" page search for "Microsoft.AspNetCore.Authentication" - you will see a great many packages, but if you scroll down a little you can find .Google, .Facebook & .Twitter.
For each social login you want to implement :
Select the appropriate NuGet package, here .Google, and click the "Install" button.
If a "Preview Change" dialog comes up, click the "OK" button.
In the "License Acceptance" dialog click the "I Accept" button.
Confirm the NuGet package was installed - in Solution Explorer expand "Dependencies" | "Packages" and look for the package, here Google.
Get picture from Google
Obtain a Google API key (which can be used for multiple API's)
Enable Google People API
Get the JSON result
Read more here : https://developers.google.com/people/api/rest/v1/people/get
The url to get the result from : https://people.googleapis.com/v1/people/GoogleUserId?personFields=names,photos&key=YouApiKey
Here is the C# function returning a url to the profile picture :
public static async Task<string> GetGoogleProfilePictureUrlAsync(string googleUserId, string apiKey)
{
/* see https://developers.google.com/people/api/rest/v1/people/get */
If you have not created you project yet, create a new project - here called Tooling
If you already have some google projects, one of these will be default selected, here in my case it is Topiqs. Click on that button.
You can now either select a project (if you have any) or create a new project - click on the "New Project" button.
Give your project a name, here I name my project Tooling, and click on the "Create" button.
Create an OAuth consent screen for the new project
Navigate to "APIs & Services" | "OAuth consent screen"
Application name : the name that the user logging in is going to see (here tooling.online)
Application logo : best to have a logo on the consent popup
Support email
Scopes : the 3 default scopes, email, profile & openid, are all what we need.
Authorized domains : this is to avoid other people is using your consent screen.
Application Homepage link
Application Privacy Policy link
Application Terms of service
Press the Save button : OAuth consent screen is created
OAuth consent screen created
Create an OAuth Client ID
Application type : select "Web application
Name : name of the client - here Tooling
Authorized redirect URIs : must end in /signin-google (since the Google Login NuGet package expect /signin-google) - here I make one for production and one for development
https://tooling.online/signin-google : for production
https://dev.tooling.online/signin-google : for development
Press the Create button : OAuth Client ID is created
sdf
Prepare Facebook
In Visual Studio add the Microsoft.AspNetCore.Authentication.Facebook Nuget package
If you have not created your Facebook app yet - create a new Facebook app by clicking on the "Add a New App" button.
In the "Create a New App ID" dialog insert the Display Name, here Tooling, and your Contact Email and then click the "Create App ID" button.
I also got a "Security Check" dialog that required me to confirm I am not a robot.
Facebook app is sucessfully created.
With your Facebook app selected, here Tooling, you should be on the "Add a Product" page - click on "Facebook Login" to add the Facebook login product to your Facebook app.
Select which platform your Facebook login should work for - click on WWW (Web)
Specify your Site URL, in my case it is https://tooling.online and then click the "Save" button.
With respect to our ASP.NET Core project, you are actually finished creating the Facebook Login product on your Facebook app now, however let's go through the rest of the Login product topics :
After saving the Site URL, click the "Continue" button to open property 2 "Set Up the Facebook SDK for Javascript".
Nothing relevant in property 2 so click on the "Next" button to continue to property 3 "Check Login Status".
Nothing relevant in property 3 so click on the "Next" button to continue to property 4 "Add the Facebook Login Button".
Nothing relevant in property 4 so click on the "Next" button to finish off ("Next Steps")
In the left side under "Facebook Login" be sure the correct Facebook app is selected, here Tooling, then under the "Facebook Login" product (which you have just created) select "Settings" and then scroll down to "Valid OAuth Redirect URIs" - here you need to input the URI's Facebook Login will redirect to AFTER Facebook have logged in the user. In my case for the Tooling website :
https://tooling.online/signin-facebook : production version
https://dev.tooling.online/signin-facebook : dev version (so that the Facebook login is also working on my development project, not only the production server).
Note that /sigin-facebook is the default callback URI of the Facebook Login NuGet package that we will install later to handle communication with the Facebook Login product.
In the left pane be sure your project (in my case Tooling) is selected and then navigate to "Settings" | "Basic" - here is a lot of information that needs to be set :
Display Name : should already be set, in my case Tooling.
Contact Email : should already be set, your email.
Privacy Policy URL : you need to have a privacy policy online for your website and point to it here, in my case https://tooling.online/privacy. Note that if Facebook cannot connect to that URI, you will not be allowed to change the status of your Facebook app from "In Development".
App Icon : not mandatory, however best to upload some icon. The icon can be either 1024x1024 or 512x512 and must also be transparent.
Category : not mandatory, however you should choose the category that best describe your website, in my case it is Utility & Productivity.
Business Use : not mandatory, however I have selected "Support my own business".
Click on "Save Changes" - if Facebook cannot connect to your privacy policy URI, the only thing getting saved is your icon.
Only step left is to switch your Facebook app status from "In Development" to "Live" (note that Facebook must have successfully connected to your Privacy Policy URI, see bullet 5.3 above, before you can switch status).
Your Facebook app status is "In Development" - click on the "OFF" button.
You are asked to confirm that you want to switch status of your Facebook app - click on the "Switch Mode" button.
Successfully changed the Facebook app status to "Live".
Prepare Twitter
Note that you MUST have a Twitter developer account to create a new Twitter app. I will not go through how to create a Twitter developer account, which while quite easy to do is also quite an annoying process especially because the account need to be manually reviewed by Twitter (for me it was done in half a day though, but I have heard of some horrible cases - embrace yourself).
If you have not created your Twitter app yet - create a new Twitter app by clicking on the "Create" button.
In the "Create an app" page, you have to input the following :
App name : here "Online Tooling" as Tooling is already taken.
Application description : describe your website.
Website URL : here https://tooling.online.
Select "Enable Sign in with Twitter".
Callback URLs : here I add the usual 2 callback urls :
https://tooling.online/signin-twitter : for production.
https://dev.tooling.online/signin-twitter : for development.
Note that /sigin-twitter is the default callback URI of the Twitter Login NuGet package that we will install later to handle communication with the Twitter Login app.
Terms of Service URL : currently not required unless you want to enable advanced permissions - here https://tooling.online/tos.
Privacy policy URL : same as Terms of Service - here https://tooling.online/privacy.
Organization name : I omit thi s since I have no organization.
Organization website URL : I also omit this.
Tell us how this app will be used : describe how your website will be used.
Click the "Create" button.
You are send to a Review screen, just press the "Create" button again.