I am a danish programmer living in Bangkok.
Read more about me @ rasmus.rummel.dk.
Webmodelling Home > ASP.NET Core Social Login - how to
Share it if you like it

ASP.NET Core Social Login

24 Jan 2017. This is the de-facto tutorial on ASP.NET Core social login, the tutorial explains step-by-step how to add social login to an ASP.NET Core web application.

Then you are finished with this tutoial, you will be able to create a login system that allows a user to choose between a standard login form or any of 5 different social login providers Facebook, Google, Twitter, LinkedIn or Microsoft (though typically you will not be interested in all of the 5 social login providers).

In addition we will enhance the system by retrieving email & profile pictures from those of the social login providers that supports it.

Index :

  1. Create a new ASP.NET Core MVC EFCore project
  2. Create an Identity based local login system
  3. Configuring the development environment - start here if you already have an Identity based login system.
  4. Configuring the social login providers
  5. Add Social Login Providers to the Login System - at last.
  6. Get profile picture from social login providers - enhance the experience.

Appendixes :

Main references :




Social Login Introduction

Tehcnically social login allows a user to use his account on another website to login to your website. If your website implements social login you will need to setup a system with a social login provider (eg. Facebook) so that a user kan click a Facebook button on your website having your website asking Facebook for whom the user is - the social login provider will manage the user authentication for you.

The reason to implement social login is that far most users perceive traditional signup forms a big hassle even if the signup form contains only an email and a password field. Not only does the user have to key in the email & password on his keyboard, the user also have to remember what email & password was used next time he visit your website and in addition the user also have to worry about the risk that your website is a scam or will be hacked exposing not only the users email to scammers but also risking the perpetrators to be able to overtake the users accounts on other websites. Social login on the other hand offers a very easy signup process of a couple of clicks with no need to key anything on the keyboard as well as lower risk and no need to remember any credentials - next time the user login only 1 click is necessary to login the user.

Other advantages of social login are that users more often than not provide false information in signup forms, that social login offers pre-validated email addresses and that social login allows your website to tap into the social login providers social graph eg. in this tutorial we will automatically fetch the users profile picture from the social login providers.

Implementation of social login is very easy with ASP.NET Core - there are Nuget packages for the main login providers, these Nuget packages abstracts away much of the communication with the social login providers. Then first you have a standard local login system (which we also build from scatch in this tutorial) adding the social login providers is actually quite easy and is little more than just plugging in the Nuget packages.

Note that in this tutorial "Social Login Provider", "External Login Provider" and "External Authentication Provider" refers to the same thing, this tutorial does not distinguish between them - I use them interchangable.




Create a new ASP.NET Core MVC EFCore project

Even though Visual Studio 2015 comes with a powerful MVC template for Identity based login, this tutorial will start with an empty template to get a better understanding of the standard login code and infrastructure that the social login code have to build upon.

  1. Create a new Visual Studio 2015 ASP.NET Core project

    1. Open Visual Studio 2015
    2. In Visual Studio create a new project eg. from the File menu.
    3. In the "New Project" dialog select the "ASP.NET Web Application" project type. Give the project a name and click the "OK" button.
    4. In the "New ASP.NET Project" dialog select the "Empty" template and click the "OK" button.
    5. Wait for Visual Studio to download all the Nuget packages specified by the "Empty" template - while downloading Nuget packages Visual Studio will show a yellow strip at the top of Solution Explorer.
    6. In Solution Explorer right click on the project name and select properties from the shortcut menu.
    7. In the project properties page select the "Debug" tab and note the App URL - we are going to change that later to support the external login providers on our development machine.
    8. The project.json file : is most importantly responsible for specifying Nuget package dependencies, however project.json as written by the "Empty" templage have by time of this writing become a little out-dated and needs to be updated - from version 1.0 to 1.1 :
      • Out-dated template (as written by Visual Studio 2015 Update 3 "Empty" templage).
      • Up-dated template (as it should be as of January 2017)

      To update project.json from version 1.0 to 1.1 do the following :

      1. From Solution Explorer open project.json
      2. Manually update the version number of all dependencies (in the dependencies section) and of the .NET Core framework as well (in the frameworks section). Then you are finished, your project.json should look like this :
        {
          "dependencies": {
            "Microsoft.NETCore.App": {
              "version""1.1.0",
              "type""platform"
            },
            "Microsoft.AspNetCore.Diagnostics""1.1.0",
            "Microsoft.AspNetCore.Server.IISIntegration""1.1.0",
            "Microsoft.AspNetCore.Server.Kestrel""1.1.0",
            "Microsoft.Extensions.Logging.Console""1.1.0"
          },
         
          "tools": {
            "Microsoft.AspNetCore.Server.IISIntegration.Tools""1.0.0-preview2-final"
          },
         
          "frameworks": {
            "netcoreapp1.1": {
              "imports": [
                "dotnet5.6",
                "portable-net45+win8"
              ]
            }
          },
         
          "buildOptions": {
            "emitEntryPoint"true,
            "preserveCompilationContext"true
          },
         
          "runtimeOptions": {
            "configProperties": {
              "System.GC.Server"true
            }
          },
         
          "publishOptions": {
            "include": [
              "wwwroot",
              "web.config"
            ]
          },
         
          "scripts": {
            "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
          }
        }
      3. Press ctrl+s to save project.json - in Solution Explorer you can see that Visual Studio is automatically downloading the new versions of the Nuget packages.
    9. The Program.cs file :
      1. From Solution Explorer open Program.cs. Program.Main is the entry point by which the ASP.NET Core App Host is created. The ASP.NET Core App Host will host your ASP.NET Core App. In Program.Main we can among others see that the Host will embed the Kestrel webserver and that the Startup class is used to configure the App.
    10. The Startup.cs file :
      • From Solution Explorer open Startup.cs. ASP.NET Core will instantiate this class and call first ConfigureServices and then Configure. In ConfigureServices we can add objects to the builtin Dependency Injection container (services), while in Configure we can add middlewares to the request pipeline. The "Empty" template have already added an in-line middleware using the Run extension function - here writing "Hello World!" to the response stream.
    11. It's time to see the Empty template in action - it is not totally empty - so press Ctrl+F5 to :
      1. compile the project.
      2. start IIS Express on the App URL
      3. open a browser on the App URL
      • We should get a Hello World!, which show us that everything is working.
    12. Add support for static & default files :

      The Visual Studio Update 3 "Empty" template does not write out support for static files, this means we will not only not be able to serve html files but also we will not be able to serve javascript, css & image files. While an MVC application typically does not need to serve html files and lots of javascript & css libraries may be CDN loaded, I generally find that no support for static files is unrealistic - therefore let's fast add support for static files and while we are at it also throw in support for default files.

      1. Open project.json from Solution Explorer and add the StaticFiles Nuget package :
        "Microsoft.AspNetCore.StaticFiles""1.1.0"
        
      2. Open Startup.cs from Solution Explorer and add the following code just BEFORE the app.AddMvc code:
        app.UseDefaultFiles();
        app.UseStaticFiles();

        Note that UseDefaultFiles MUST come before UseStaticFiles. UseDefaultFiles is an internal url rewriter that if the url does not have any path (pointing to any resource) will search your wwwroot for default files like eg. index.html and if found rewriting the path-less url to [url]/index.html eg. http://mydomain.com to http://mydomain.com/index.html. UseStaticFiles is a middleware that will intercept static files request like eg. http://mydomain.com/index.html and if found send them with a 200 Ok status code.

      3. The application now supports static and default files.
  2. Make the project an MVC project

    Currently the only web applications you can make with ASP.NET Core is an MVC web application and a WebApi application, especially you cannot make a web forms application. In addition in ASP.NET Core, MVC and WebApi are technically the same thing though their intended uses are different. Ok we need to setup the MVC infrastructure (if then creating the project we had chosen the MVC Application template, the MVC infrastructure would have been setup automatically, however since we chose the "Empty" template we have to setup the MVC infrastructure ourself).

    1. First we want to add the MVC dependencies :
      1. Open project.json from Solution Explorer and add the following MVC dependencies :
        • "Microsoft.AspNetCore.Mvc""1.1.0" : package containing the core MVC 6 libraries.
        • "Microsoft.AspNetCore.Mvc.TagHelpers""1.1.0" : package containing the new MVC 6 TagHelpers (which will use later)
    2. We then want to code the Startup class.
      1. Open Startup.cs from Solution Explorer.
      2. In the Startup.ConfigureServices method register the MVC services with the DI container by using the AddMvc extension function : (do NOT use the AddMvcCore function, which will register only a few of many necessary services guaranteeing you run into problems)
        services.AddMvc();
      3. Delete the default response "Hello World!" middleware that the Visaul Studio 2015 "Empty" template added to the Startup.Configure method.

        (Visual Studio 2015 "Empty" template uses the IApplicationBuilder Run extension method to insert a middleware that sends a "Hello World!" to the browser - this middleware is perfect to confirm whether our initial project is working, however we don't need that middleware anymore after that).

      4. In the Startup.Configure method use the IApplicationBuilder UseMvc extension method to add the MVC middleware to the request pipeline.
        app.UseMvc(routes =>
        {
        	routes.MapRoute(
        		name: "default",
        		template: "{controller=Home}/{action=Index}/{id?}");
        });
        

        Note that there is a new extension function called UseMvcWithDefaultRoute that comes the a default route identital to the route we defined above (and also called default) - you can use UseMvcWithDefaultRoute instead if you want.

    3. Add the following MVC relevant folder structure under the project node.
      • Controllers
      • Entities
      • ViewModels
        • Account
      • Views
        • Account
        • Home
        • Shared

      Note that instead of the infamous Models folder, I have an Entities and a ViewModels folder : this is more practical and are starting to become a trend - the ViewModels folder will hold all models consumed by views (typically more flat models), while the Entities folder will hold all models describing the data store (typically more structured models).

    4. Add the basic MVC files : (to create a Hello MVC!)
      1. Add an Index.cshtml file in the Views\Home folder.
      2. In the Index.cshtml file delete any template content and add the following :
        <div>
        	Hello MVC!
        </div>

        In earlier versions of MVC we would specify a _Layout file in all of our views, however since MVC6 and now ASP.NET Core MVC, best practice is to default load all views with a layout file using the new _ViewStart.cshtml file, which the MVC system will automatically prefix all views.

      3. Add a _Layout.cshtml file in the Shared folder.
      4. Add a few references to the _Layout.cshtml template code to help us enhance the user experience :
        • A base jQuery javascript reference allowing us to reference all kinds of jQuery based frontend enhancements (in this tutorial we will use it for validation enhancement) :
          <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.1.1.min.js"></script>
        • A bootstrap stylesheet reference allowing us to easily make our web pages more nice to look at and more easy to understand :
          <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css" />
        • And the bootstrap javascript reference :
          <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"></script>
        • Allow any View using this _Layout.cshtml file to embed a Scripts section :
          @RenderSection("scripts", required: false)
      5. Add a _ViewStart.cshtml file in the Views folder to get the _Layout.cshtml file loaded automatically by our views (otherwise we would had to specify the layout file in all of our view files.
      6. Keep the template code of the _ViewStart.cshtml file.
      7. Like the _ViewStart.cshtml specifies a default layout file (so we don't need to write code in every view to load the _Layout.cshtml file), ASP.NET Core MVC (as well as MVC-6) also supports a _ViewImports.cshtml file there we can write what dependencies to default load for every view.
        1. Add a _ViewImports.cshtml file in the Views folder
        2. Import the MVC Tag Helpers library so they become available in every view we create :
          @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
          

          In this tutorial we only use _ViewImports.cshtml to get MVC Tag Helpers automatically loaded, however in a real project we would typically load several kinds of resources into _ViewImports.cshtml, eg. project namespaces used most often.

      8. Add a HomeController.cs file in the Controllers folder.
      9. Keep the template code of the HomeController.cs file.
    5. It's time to see the MVC setup in action - so press Ctrl+F5 to :
      1. compile the project.
      2. start IIS Express on the App URL
      3. open a browser on the App URL
      • We should get a Hello MVC!, which show us that the MVC infrastructure is working.
  3. Add an application configuration file - appsettings.json

    In ASP.NET Classic we would use web.config to configure an asp.net application, however with ASP.NET Core the configuration have changed to json files and is now more flexible because it is easy to merge multiple configuration files typically for production, staging & development but also for specific aspects eg. in a project I am working on, bizRiot.com, I among others have a configuration file called angularsettings.json to configure a custom middleware for angular2. However while in a real project we will typically merge several configuration files, in this tutorial we will only create one configuration file appsettings.json :

    1. In Solution Explorer add a new item.
    2. In the "Add New Item" dialog, select "ASP.NET Configuration File", accept the default name appsettings.json and click the "Add" button.
    3. The appsettings.json file comes with a small template containing a ConnectionStrings section.
    4. In the "ConnectionStrings:DefaultConnection" section change _CHANGE_ME to Tutorial-SocialLogin (or to the name of your own project). You appsettings.json should now look like this :
      {
        "ConnectionStrings": {
          "DefaultConnection""Server=(localdb)\\MSSQLLocalDB;Database=Tutorial-SocialLogin;Trusted_Connection=True;MultipleActiveResultSets=true"
        }
      }
    5. Add configuration related Nuget packages :
      1. From Solution Explorer open project.json.
      2. Add the following 2 configuration related Nuget packages :
        • "Microsoft.Extensions.Configuration.FileExtensions""1.1.0"
        • "Microsoft.Extensions.Configuration.Json""1.1.0"
    6. Load appsettings.json in Startup.cs
      1. From Solution Explorer open the Startup.cs file.
      2. Add an IConfigurationRoot field called Configuration to the Startup class.
        private IConfigurationRoot Configuration;
      3. Add a Startup constructor and initialize the Configuration field using a ConfigurationBuilder
        public Startup(IHostingEnvironment env)
        {
        	var builder = new ConfigurationBuilder()
        		.SetBasePath(env.ContentRootPath)
        		.AddJsonFile("appsettings.json");
         
        	Configuration = builder.Build();
        }

    All values from appsettings.json are now programmatically available through the Configuration field. Later we will user the Configuration field to load social login provider ID's & Secrets from appsettings.json.

    Note that we are injection IHostingEnvironment object into the Startup construction so we can retrieve the ContentRootPath and thereby telling the ConfigurationBuilder where to start looking for any configuration files we specify (here only appsettings.json).

  4. Add Entity Framework - EntityFrameworkCore

    Entity Framework is an ORM framework created by Microsoft and have existed for several years with its latest iteration being version 7 however as ASP.NET 5 was renamed to ASP.NET Core and MVC 6 to ASP.NET Core MVC also Entity Framework 7 was renamed to EntityFrameworkCore. Using LINQ Entity Framework allows us to work with data as strongly typed objects instead of ADO.NET Collections from SQL and also newer versions of Entity Framework supports a "Code First" approach to creating the database - we model the data store using POCO classes and then let the Entity Framework create the database, tables, keys, indexes and other database objects for us automatically as we go along - this is now the common way to do it.

    The central EntityFrameworkCore class is DbContext which represent the database. Generally we need to extend DbContext from a generic type to a project specific type reflecting the project data needs - the specific type is by convention called ApplicationDbContext.

    1. In Solution Explorer open the project.json file and add the following Entity Framework dependencies to the project.json file :
      • "Microsoft.EntityFrameworkCore""1.1.0"
      • "Microsoft.EntityFrameworkCore.SqlServer""1.1.0"
      • "Microsoft.EntityFrameworkCore.Tools""1.0.0-preview2-final"
    2. Also add the EntityFrameworkCore.Tools package to the project.json tools section : (this allows us to update the database from within Visual Studio Package Manager instead of using the dotnet tool from a command shell)
      • "Microsoft.EntityFrameworkCore.Tools""1.0.0-preview2-final"
    3. Create a new ApplicationDbContext.cs file in the Entities folder.
    4. Make the ApplicationDbContext class inherit from DbContext (later then adding Identity to the project, we need to change ApplicationDbContext to inherit from IdentityDbContext instead of DbContext).
    5. Also since we need to pass some options to DbContext then registering ApplicationDbContext with the DI Container (see next), we need to add a constructor that accepts such options and pass them to DbContext. So add the following constructor to ApplicationDbContext :
      public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
      { }
      

      If we did not add the constructor, we will inevitably run into the following error No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.
      What the error says is that IF the AddDbContext is used (and indeed the next thing we will do just below is to AddDbContext to the DI Container in Startup.ConfigureServices), THEN our custom DbContext (ApplicationDbContext) must accept a DbContextOptions<OurCustomDbContext> in it's constructor and pass that argument to the generic DbContext base constructor.

    6. From Solution Explorer open Startup.cs and in Startup.ConfigureServices add the Entity Framework service to the DI Container :
      services.AddEntityFrameworkSqlServer()
      	.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));

      Note the AddDbContext - we can only do that if our custom DbContext (ApplicationDbContext) have a constructor that accepts a DbContextOptions<CustomDbContext> and passes it to the generic DbContext base constructor.

    Entity Framework is now added and we can start creating data models and add these models to our ApplicationDbContext and that is exactly what we will do in the next section "Create an Identity based local login system".



Create an Identity based local login system

Out tutorial project, Tutorial_SocialLogin, now have enough of an infrastructure that we can start programming some Identity based local login system - later we will expand that local login system with external login providers.

Identity is a well designed and easily extensible group of classes for managing users - if you have not yet adopted Identity and still uses your own custom rolled antique user system (like I did), the arrival of ASP.NET Core is a good occasion to switch to Identity, here are 3 good reasons :

  • Identity group of classes makes it easy to manage users, especially :
    • IdentityUser : represents a user. Typically we extend the generic type IdentityUser to a custom type specific to the application we are building. The extended IdentityUser class is by convention called ApplicationUser - like this :
      public class ApplicationUser : IdentityUser
    • IdentityDbContext : extends DbContext thereby representing the database. IdentityDbContext have builtin models to create the database tables necessary for IdentityUser. As we have already seen above in the "Add Entity Framework - EntityFrameworkCore", we typically extend DbContext in a custom class called ApplicationDbContext to represent the database specific for our application. However then working with Identity we want to extend IdentityDbContext instead of DbContext - like this :
      public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
      In addition we bind IdentityDbContext to ApplicationUser so that the models (that translates to database tables) are build to the specification of our custom user ApplicationUser not to the specification of the generic user IdentityUser.
    • SignInManager : signes users in & out.
    • UserManager : creates & deletes users and manages claims and passwords.
  • Identity automatically works together with SignalR (SignalR is currently only available for ASP.NET Core on top of .NET Framework not on top of .NET Core )
  • Identity will undoubtly be the standard way asp.net programmers build users systems.
  1. Add Identity

    1. In Solution Explorer open the project.json file and add the Identity dependencies to the project.json file.
      • "Microsoft.AspNetCore.Identity""1.1.0"
      • "Microsoft.AspNetCore.Identity.EntityFrameworkCore""1.1.0"
    2. Create a new ApplicationUser.cs file in the Entities folder.
    3. Let the ApplicationUser class inherit from IdentityUser and add a ProfilePictureUrl string property to the class :
      • public string ProfilePictureUrl { getset; }
    4. In Solution Explorer open ApplicationDbContext.cs in the Entities folder and change ApplicationDbContext to inherit from IdentityDbContext instead of DbContext. IdentityDbContext needs to bind to our newly created ApplicationUser so that any custom user properties (in our case ProfilePictureUrl) can be added to the user table in the database. The ApplicationDbContext class should now look like this :
      public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
      {
      	public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
      	{ }
      }
      

      What IdentityDbContext does is to add Identity tables to the database, that is : tables to hold all the Identity default user information as well as all our custom user properties - in our case ProfilePictureUrl.

      Also IdentityDbContext will out-of-box prepare tables for handling external login providers - which is an added bonus of Identity much appreciated as handling external login providers is exactly what we aim to do in this tutorial.

    5. Open Startup.cs from Solution Explorer and in Startup.ConfigureServices add relevant Identity objects to the DI Container :
      services.AddIdentity<ApplicationUserIdentityRole>(options =>
      {
      	options.Password.RequireDigit = false;
      	options.Password.RequireLowercase = false;
      	options.Password.RequireUppercase = false;
      	options.Password.RequireNonAlphanumeric = false;
      })
      	.AddEntityFrameworkStores<ApplicationDbContext>() // adds SignInManager & UserManager to the DI Container
      	.AddDefaultTokenProviders(); // for account operations like password reset and two-factor authentication

      The above code also shows how to configure the Identity system, here how to configure some password requirements then signing up locally.

    6. After registering relevant Identity objects with the DI Container, we also need to register the Identity middleware with the request pipeline - we do that in Startup.Configure just BEFORE the app.UseDefaultFiles :
      app.UseIdentity();
      
  2. Creating the database

    Our database creation methodology is code-first - that is: we create a model consisting of some classes and then we use EntityFrameworkCore tools to create the database based on that data model. Within the context of Entity Framework, the process of coming from a data model to a database is called migration.

    Then working with Identity a small data model is already defined automatically for us by the IdentityDbContext class. One of the classes making up the data model defined by IdentityDbContext is IdentityUser, which then migrating will result in a database table called AspNetUsers. We added a little to the data model ourself then we amended the IdentityUser with a property called ProfilePictureUrl, which then migrating will result in a column on the AspNetUsers table called ProfilePictureUrl.

    Since we already have a data model and we also specified a DefaultConnectionString in project.json (which will be automatically handed to the migration tool), we can start a migration.

    1. Run the migrations :
      1. Open project.json and notice the EntityFrameworkCore.Tools reference in the tools section - this reference allows us to execute migration commands directly from the Package Manager Console from within Visual Studio (so that we don't need to open a command shell and execute the migration commands from there and also in the command shell we are forced to use the dotnet command which is more verbose compared to the commands we can use in Package Manager Console).

        However, since Package Manager Console binds to project.json tools section on Visual Studio startup, you need to be sure that you have restarted Visual Studio AFTER you have added the EntityFrameworkCore.Tools reference to the project.json tools section - if not then restart Visual Studio now.

      2. Compile your project (the migration commands will work on your binaries not your code files).
      3. Open Package Manager Console. If it is not already open, then you can open it from "Main Menu -> View -> Other Windows -> Package Manager Console".
      4. PM> add-migration Initial : this command will create a code file called Initial (with a datetimestamp prefixed) that then executed will create database tables (as translated from your data model).

        In Solution Explorer you can see that a Migrations folder have been created under your project node (this happens automatically first time you create a migration file) and that the Migrations folder contains your migration file - Initial.

      5. PM> update-database : this command will compile and execute any migration files not yet in effect (in our case Initial).

        If no database exists then the command will also create the database based on the connectionstring defined for the DbContext (in our case ApplicationDbContext) added to Entity Framework - we have already done that in Startup.ConfigureServices :

        Open Sql Server Object Explorer and expand "SQL Server -> (localdb)\MSSQLLocalDB -> Databases", you can see that your database have been created (in our case Tutorial-SocialLogin).

    2. In SQL Server Object Explorer open the Tutorial-SocialLogin database to see the tables - you should see 7 tables corresponding to the Identity data model (there is also 1 table created for migration maintenance, however that table have nothing to do with our data model) :
      • AspNetRoleClaims :
      • AspNetRoles :
      • AspNetUserClaims :
      • AspNetUserLogins : Associates external login providers with records in the AspNetUsers table.
      • AspNetUserRoles :
      • AspNetUsers : The main user table holding a record for each user.
      • AspNetUserTokens :
    3. Right click on the AspNetUsers table and select "View Designer" from the shortcut menu - this will open AspNetUsers in design view.
    4. You can see that our custom property, ProfilePictureUrl, defined in ApplicationUser have been added as a column in the AspNetUsers table - this has happened because we bound IdentityDbContext to ApplicationUser thereby adding ProfilePictureUrl to the IdentityUser model :
      public class ApplicationDbContext : IdentityDbContext<ApplicationUser>

    With both Identity added and the database created we are now ready to code the local login system.

  3. The local login system

    A full scale local login system is quite involved and may at least consist of the following pages :

    • Register page
    • Confirm email page
    • Login page
    • Forgot password page
    • Forgot password confirmation page
    • Reset password page
    • Reset password confirmation page

    However, in this tutorial I will be focusing only on the pages relevant for extending the local login system with external login providers :

    • Login page
    • Register page
  4. Coding the Login page

    As usual an MVC page consist of at least 3 pieces : a model, a controller and a view. However in ASP.NET Core MVC we use ViewModels for the models used by views - a model is nothing but a class with properties however models relating to a database tends to be more normalized while models relating to views tends to be more flat or denormalized. So while there is no technical difference between a Model and a ViewModel (both are classes with properties and both are modelling the data of our domain) it is more easy to code if we organize our view related models in a folder of it's own - the ViewModels folder (to be created shortly).

    The Login page consist of these pieces :

    • LoginViewModel.cs : holding the model of the login page.
    • AccountController.cs : holding a GET and a POST version of a Login action method. The GET version we request to load the login form and the POST version we post to in order to attempt to login.
    • Login.cshtml : holding the view of the login page.
    1. Login page -> LoginViewModel
      1. Add a new folder structure under the project folder
        • ViewModels
          • Account
      2. In Solution Explorer add a LoginViewModel.cs file in the ViewModels\Account folder.
      3. Add 3 properties to the LoginViewModel class : Email, Password & RememberMe :
        [Required]
        [EmailAddress]
        public string Email { getset; }
         
        [Required]
        [DataType(DataType.Password)]
        public string Password { getset; }
         
        [Display(Name = "Remember me?")]
        public bool RememberMe { getset; }
        

        The data annotations helps us write less code, eg. the [EmailAddress] annotation automates email validation while the [DataType(DataType.Password)] annotation tells the View consuming this ViewModel to display an input box of type password for any input box bound to the Password property of the ViewModel and in addition validate these input boxes according to password rules (some of which we configured in Startup.ConfigureServices above then configuring the Identity service).

    2. Login page -> AccountController

      In the AccountController we write the login related endpoints - that is: a function to respond on the GET request for the login form and a function to handle the POST request with the filled out login form.

      In Startup.Configure we added an MVC middleware on which we configured a route template (default) so that if we have an AccountController with a function called Login, that Login function can be reached by a request like this : /account/login.

      1. In Solution Explorer add a new AccountController.cs file to the Controllers folder (by convention the controller for user management is called AccountController).
        1. Delete the Index function from the AccountController tmeplate code.
        2. Constructor inject UserManager & SignInManager into AccountController by adding the following code :
          private readonly UserManager<ApplicationUser> _userManager;
          private readonly SignInManager<ApplicationUser> _signInManager;
           
          public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
          {
          	_userManager = userManager;
          	_signInManager = signInManager;
          }
          

          Then we added the Identity service in Startup.ConfigureServices both UserManager & SignInManager was added to the Dependency Injection Container and upon request the ASP.NET Core framework will automatically pass any object in the DI Container - in this case pass UserManager & SignInManager then instantiating AccountController.

        3. To handle login we need to write 2 functions : 1 function for getting the Login page (endpoint for a GET request) and 1 function for posting the Login page (endpoint for a POST request).
          1. Right after the AccountController constructor add the GET Login function :
            // GET: /Account/Login
            [HttpGet]
            [AllowAnonymous]
            public IActionResult Login(string returnUrl = null)
            {
            	ViewData["ReturnUrl"] = returnUrl;
            	return View();
            }
            
          2. Right after the AccountController GET Login function add the POST Login function :
            // POST: /Account/Login
            [HttpPost]
            [AllowAnonymous]
            [ValidateAntiForgeryToken]
            public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
            {
            	ViewData["ReturnUrl"] = returnUrl;
            	if (ModelState.IsValid)
            	{
            		// Try to signin using the injected SignInManager
            		var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
            		if (result.Succeeded)
            		{
            			// If the user came from another page but was redirected to login, then redirect the user back to that page, otherwise redirect user to main page
            			if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); }
            		}
            		else
            		{
            			ModelState.AddModelError("User""Invalid login attempt.");
            		}
            	}
             
            	// If either ModelState was invalid or the login attempt failed then redisplay the login form
            	return View(model);
            }
            
    3. Login page -> Login View
      1. In Solution Explorer add a new Login.cshtml file to the Views/Account folder.
      2. In Login.cshtml delete all the template code and instead write the following code :
        @model Tutorial_SocialLogin.ViewModels.Account.LoginViewModel
         
        <h2>Login</h2>
        <div class="row">
        	<div class="col-md-8">
        		<section>
        			<!-- Local Login Section-->
        			<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
        				<h4>Use a local account to log in.</h4>
        				<hr />
        				<div asp-validation-summary="All" class="text-danger"></div>
        				<div class="form-group">
        					<label asp-for="Email" class="col-md-2 control-label"></label>
        					<div class="col-md-10">
        						<input asp-for="Email" class="form-control" />
        						<span asp-validation-for="Email" class="text-danger"></span>
        					</div>
        				</div>
        				<div class="form-group">
        					<label asp-for="Password" class="col-md-2 control-label"></label>
        					<div class="col-md-10">
        						<input asp-for="Password" class="form-control" />
        						<span asp-validation-for="Password" class="text-danger"></span>
        					</div>
        				</div>
        				<div class="form-group">
        					<div class="col-md-offset-2 col-md-10">
        						<div class="checkbox">
        							<input asp-for="RememberMe" />
        							<label asp-for="RememberMe"></label>
        						</div>
        					</div>
        				</div>
        				<div class="form-group">
        					<div class="col-md-offset-2 col-md-10">
        						<button type="submit" class="btn btn-default">Log in</button>
        					</div>
        				</div>
        				<p>
        					<a asp-action="Register">Register as a new user?</a>
        				</p>
        			</form>
        		</section>
        	</div>
        	<div class="col-md-4">
        		<section>
        			<!-- Social Login Section -->
        		</section>
        	</div>
        </div>
         
        @section Scripts {
        	@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
        }

        Notice all the MVC Tag Helpers (asp-controller, asp-action, asp-for etc.) which are available because we imported the MVC Tag Helpers in the _ViewImports.cshtml file earlier. These Tag Helpers (introduced in MVC-6) are an improvement over the Html Helpers used in earlier versions of ASP.NET MVC because Tag Helpers are easier readable and because Visual Studio 2015 have intellisense support for them.

      3. Componentize the Scripts section

        It is common to use jQuery validation to enhance the users validation experience and for this we just need to reference 2 jQuery files. These 2 jQuery references could be put directly in the Login.cshtml file above, however since it is common to use the same validation logic on many different pages it is best to make a reuseable component of the validation - in our case to put the 2 jQuery references in its own file and then reference that file on all pages there validation is needed. Should we later want to change the validation code we, can change the code only once in the component and all pages using it will be updated - this helps making validation updates easy & consistent.

        In Login.cshtml above we have already referenced the validation component to be : _ValidationScriptsPartial

        1. In Solution Explorer add a new file _ValidationScriptsPartial.cshtml to the the Views\Shared folder.
        2. Delete the template code from the _ValidationScriptsPartial.cshtml file and add the following to jQuery validation references instead :
          <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.15.1/jquery.validate.min.js"></script>
          <script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js"></script>

        The clientside validation scripts are working on HTML attributes written by the MVC Tag Helpers, eg. say that you have a login form with an input field for username and the LoginViewModel Username property is decorated with the [Required] attribute, then MVC Tag Helpers will amend the username html input field with an attribute data-val-required="The Username field is required". The jQuery validation scripts knows these attributes and will trigger on them. You can read more about how this works here : https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation.

        Also notice that in the above Login.cshtml for Html.RenderPartialAsync we do not need to specify the file extension (cshtml) and also we do not need to specify the location of the file - Html.RenderPartialAsync looks for a partial view in Views\Shared, it is not even possible to specify an alternative path (it is possible though to pass a ViewBag or a Model).

    4. Confirm the Login UI
      1. In Visual Studio 2015 press ctrl+F5
      2. In the browser opened by Visual Studio you should see the Hello MVC! message (loaded from Home/Index)
      3. In the browser url field append /Account/Login and press Enter to load the new url - you should now see the Login UI.
  5. Coding the Registration page

    Like the Login page, the Registration page also consists of the standard 3 MVC pieces :

    • RegisterViewModel.cs : holding the model of the registration page.
    • AccountController.cs : holding a GET and a POST version of a Register action method. The GET version we request to load the registration form and the POST version we post to in order to attempt to registrate.
    • Register.cshtml : holding the view of the registration page.
    1. Registration page -> RegisterViewModel
      1. In Solution Explorer add a RegisterViewModel.cs file to the ViewModels\Account folder.
      2. Add 3 properties to the RegisterViewModel class : Email, Password & ConfirmPassword :
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { getset; }
         
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { getset; }
         
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { getset; }
        

        Notice the StringLength annotation for the Password field, which is also a good idea because it allows clientside validation of the Password length. However the StringLength annotation will will need to set the MinimumLength attribute which we already sat once then adding Identity to the DI Container in Startup.ConfigureServices - this is not so good that we have to set the same value 2 very different places.

    2. Registration page -> AccountController

      To handle registration we need to add 2 functions to AccountController : 1 function for getting the Registration page and 1 function for posting the Registration page (form).

      1. In Solution Explorer open AccountController.cs from the Controllers folder.
      2. At the bottom of the AccountController class (right after the POST Login function) add the GET Register function :
        // GET: /Account/Register
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Register()
        {
        	return View();
        }
        
      3. Right after the AccountController GET Register function add the POST Register function :
        // POST: /Account/Register
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Register(RegisterViewModel model)
        {
        	if (ModelState.IsValid)
        	{
        		var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
        		var result = await _userManager.CreateAsync(user, model.Password);
        		if (result.Succeeded)
        		{
        			await _signInManager.SignInAsync(user, isPersistent: false);
        			return RedirectToAction(nameof(HomeController.Index), "Home");
        		}
         
        		foreach (var error in result.Errors)
        		{
        			ModelState.AddModelError("User", error.Description);
        		}
        	}
         
        	// If either ModelState was invalid or the login attempt failed then redisplay the registration form
        	return View(model);
        }
        

        The important code is the _userManager.CreateAsync function which will create a user (an ApplicationUser) as a record in the AspNetUsers table. If the creation succeeded the code will login the user with the credentials just created and return him to /home/index. If the creation failed, the code will add all errors to ModelState (which in turn will trigger MVC Tag Helpers to emit html attributes for error handling by the jQuery validation scripts) and return the same form filled out with the posted values (of the RegisterViewModel model instance).

    3. Registration page -> Register View
      1. In Solution Explorer add a Register.cshtml file to the Views\Account folder.
      2. In Register.cshtml delete all the template code and instead write the following code :
        @model Tutorial_SocialLogin.ViewModels.Account.RegisterViewModel
         
        <h2>Register</h2>
         
        <form asp-controller="Account" asp-action="Register" method="post" class="form-horizontal" role="form">
        	<h4>Create a new account.</h4>
        	<hr />
        	<div asp-validation-summary="All" class="text-danger"></div>
        	<div class="form-group">
        		<label asp-for="Email" class="col-md-2 control-label"></label>
        		<div class="col-md-10">
        			<input asp-for="Email" class="form-control" />
        			<span asp-validation-for="Email" class="text-danger"></span>
        		</div>
        	</div>
        	<div class="form-group">
        		<label asp-for="Password" class="col-md-2 control-label"></label>
        		<div class="col-md-10">
        			<input asp-for="Password" class="form-control" />
        			<span asp-validation-for="Password" class="text-danger"></span>
        		</div>
        	</div>
        	<div class="form-group">
        		<label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
        		<div class="col-md-10">
        			<input asp-for="ConfirmPassword" class="form-control" />
        			<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
        		</div>
        	</div>
        	<div class="form-group">
        		<div class="col-md-offset-2 col-md-10">
        			<button type="submit" class="btn btn-default">Register</button>
        		</div>
        	</div>
        </form>
         
        @section Scripts {
        	@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
        }
        

        Notice we are using the same clientside validation component, _ValidationScriptsPartial, for the registration form as we did for the login form.

    4. Confirm the Registration UI
      1. In Visual Studio 2015 press ctrl+F5
      2. In the browser opened by Visual Studio you should see the Hello MVC! message (loaded from Home/Index)
      3. In the browser url field append /Account/Register and press Enter to load the new url - you should now see the Registration UI.
  6. Login link and login status

    Currently our default page (Home/Index) only contains a Hello MVC!, there is no link to the login or register page and in addtion the user cannot see whether he is logged in or not. The typical solution is to code a small component, let's call it LoginStatus, that indeed shows the login status and a context relevant link to either login or logout - the most basic form of such a LoginStatus component will be like this :

    • The user is logged in : show a link to logout.
    • The user is not logged in : show a link to the login page.
    1. Coding the LoginStatus component
      1. In Solution Explorer add a LoginPartial.cshtml file to the Views\Shared folder (I would have liked to call this file LoginStatus, however it seems to be standard to call it LoginPartial so for this tutorial I will follow).
      2. In LoginPartial.cshtml delete the template code and instead add the following code :
        @using Microsoft.AspNetCore.Identity
        @using Tutorial_SocialLogin.Entities
         
        @inject SignInManager<ApplicationUser> SignInManager
        @inject UserManager<ApplicationUser> UserManager
         
        @if (SignInManager.IsSignedIn(User))
        {
        	<form asp-controller="Account" asp-action="Logout" method="post" id="logoutForm" class="navbar-right">
        		<ul class="nav navbar-nav navbar-right">
        			<li>
        				<a href="#">Hello @UserManager.GetUserName(User)</a>
        			</li>
        			<li>
        				<button type="submit" class="btn btn-link navbar-btn navbar-link">Log off</button>
        			</li>
        		</ul>
        	</form>
        }
        else
        {
        	<ul class="nav navbar-nav navbar-right">
        		<li><a asp-controller="Account" asp-action="Register">Register</a></li>
        		<li><a asp-controller="Account" asp-action="Login">Log in</a></li>
        	</ul>
        }

        Unfortunately Intellisense cannot suggest namespaces in View pages, that's why I show the @using statements in the code. SignInManager & UserManager is in Microsoft.AspNetCore.Identity and ApplicationUser is in Tutorial_SocialLogin.Entities.

        The LoginPartial view contains 3 links we need to code action methods for :

        • In case the user is logged in :
          • Account/Logout : the action attribute of the form (the View compiler will transpile asp-controller & asp-action tag helpers in forms to a standard form action attribute) - action method not yet created.
        • In case the user is NOT logged in :
          • Account/Login : action method already created earlier.
          • Account/Register : action method already created earlier.

        So the only link in the LoginPartial view we have not yet coded an action method for is the Logout button - let's do that now.

      3. In Solution Explorer find and open the AccountController.cs file.
      4. Add the following code at the bottom of the AccountController class just after the POST Register method :
        // POST: /Account/Logout
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout()
        {
        	await _signInManager.SignOutAsync();
        	return RedirectToAction(nameof(HomeController.Index), "Home");
        }
        
    2. Displaying the LoginStatus UI

      Most often a LoginStatus UI is placed in a top bar across all (or most) pages in a website or web application - so let's do that :

      1. In Solution Explorer opten the _Layout.cshtml file from the Views\Shared folder.
      2. Add a fast bootstrap based top bar that will apply to all views using the _Layout.cshtml file - so amend the _Layout.cshtml file with the following code :
        • Add a style attribute to the body tag setting the margin-top to 50px :
          <body style="margin-top:50px;">
          
        • Add the top bar markup just under the body tag :
          <div class="navbar navbar-inverse navbar-fixed-top">
          	<div class="container">
          		<div class="navbar-collapse collapse">
          			@await Html.PartialAsync("LoginPartial")
          		</div>
          	</div>
          </div>

        Notice that I use Html.PartialAsync while in the Login.cshtml & Register.cshtml Scripts section I used Html.RenderPartialAsync. The difference is that Html.Partial returns HTML which embeds into the View and therefore is handled by the View compiler while Html.RenderPartial returns void and writes directly to Response stream bypassing the View compiler (the Async versions are both returning a Task however the task resolves to either IHtmlContent or void respectively). Since the Scripts sections in Login.cshtml & Register.cshtml does not contain any content relevant for the View compiler, it gives good meaning to bypass the View compiler and embed the script references directly into the Response stream.

    As part of implementing the social login providers later, we will enhance the LoginStatus component to also show a profile picture in the cases there a profile picture can be obtained from the external login provider.

  7. Trying out the local login system

    We now have a simple user system however enough that we can start testing it - registering and logging in

    1. In Visual Studio 2015 press ctrl+F5 to compile and load the project url in a browser tab.
    2. In the browser opened by Visual Studio you should now see not only the Hello MVC! message (loaded from Home/Index) but also a Register and a Login link (emitted by the LoginStatus plugin coded in the LoginStatus.cshtml file and loaded by the _Layout.cshtml file).
    3. Testing registration
      1. Click on the Register link in the top right corner to navigate to the Registration page (Account/Register).
      2. Fill in Email, Password & Confirm password and click on the Register button - the AccountController.Register method will not only register you but also log you in and then redirect you to the home page.
      3. After registering your browser have redirected to the home page and you should be able to see that the LoginStatus UI have changed to display a Log off link and to notify you that you are logged in by displaying your Username (Email).
      4. Click on the Log off button to log out.
      5. In Visual Studio open Sql Server Object Explorer and navigate to the AspNetUsers table.
      6. Right click on the AspNetUsers table node and select View Data from the shortcut menu - in the AspNetUsers table view you should be able to see the record created for the user you just registered.
    4. Testing login
      1. Back in the browser click on the Login link in the top right corner to navigate to the Login page (Account/Login).
      2. Fill in Email & Password and click the Login button - the AccountController.Login method will login you in and redirect you to your home page (actually the Login method will first evaluate whether you came from another local page and if you did then redirect you to that page, however in this tutorial we will not explore that functionality).
      3. After logging in your browser have redirected to the home page and you should be able to see that the LoginStatus UI have changed to display a Log off link and to notify you that you are logged in by displaying your Username (Email).
    5. Testing validation
      1. Be sure that you are logged out.
      2. Click on the Register link to navigate to the Register page.
      3. Without filling in any fields click the Register button - you should now see some validation messages.

    Congratulations - your local login system should work now.



Configuring our development environment

With a functioning login system in place it is at last time to start expanding the system with external login providers. However we first need to configure our development environment to secure that we can get the external login working also on our development environment.

While in general the social login providers have excellent and consistent support for production environment, the providers are less excellent and less consistent with regard to a development environment. Here are the main pain points : (for a development environment)

  • Microsoft does NOT support port numbers.
  • Twitter does NOT allow localhost.

The default development App Url in Visual Studio is http://localhost:xxxx, however this App Url is NOT supported by Microsoft nor Twitter. Instead of localhost:xxxx we need to configure our development environment to use Some.Domain.Name for App Url. The best setup is to start with a real production domain and then create a development domain by prefixing the production domain with dev. - like this :

  • somedomain.com - production environment.
  • dev.somedomain.com - development environment.

As we should use real domains (and not fake domains like somedomain.com) I will for this tutorial use bizriot.com as I have already setup social login for my bizriot.com project, however you should choose a domain that you own yourself or a domain you are working on. This means that in my case and for the face of this tutorial, I will use the following :

  • bizriot.com - production environment.
  • dev.bizriot.com - development environment.

(Note that using the dev.bizriot.com domain means that any local webserver using port 80 (or any other program using port 80 eg. Skype) must be stopped)

Developing in Visual Studio using dev.bizriot.com (instead of localhost:xxxx) requires a change to Visual Studio project configuration file as well as to Windows hosts file :

  1. Visual Studio development configuration :
    1. In Visual Studio Solution Explorer right click on the project name and select Properties from the shortcut menu.
    2. In the properties page select Debug and set the App URL to dev.bizriot.com (or dev.yourdomain.name).
  2. hosts file domain configuration :
    1. Open a file explorer and navigate to C:\Windows\System32\drivers\etc
    2. Open the C:\Windows\System32\drivers\etc\hosts file in a text editor.
    3. Add the following to the bottom of the hosts file :
      • 127.0.0.1 dev.bizriot.com
    4. After saving your hosts file, windows domain name lookup service will now return 127.0.0.1 (localhost) every time you request the IP of dev.bizriot.com. So then you request dev.bizriot.com in your browser, the browser will actually ask the webserver on your localhost to load dev.bizriot.com.

Your development environment is now ready and you can start configuring the actual social login providers : Facebook, Google, Twitter, LinkedIn & Microsoft.



Configuring the social login providers

From each social login provider we need to obtain a key and a secret (often called ClientId & ClientSecret) - the key identifies the application (in my case bizRiot) and the secret secures that the application (bizRiot) cannot be impostored. We need a place to store the key and the secret, in this tutorial I store them in the main configuration file appsettings.json (if you are using a public version control store like eg. the free version of Github, then you should NOT store any secrets in any file partaking in the version control).

Prepare the appsettings.json file to store keys & secrets for each of the social login providers :

  1. Open the appsettings.json file from Solution Explorer.
  2. To the appsettings.json file add an "Authentication" section and then sub-sections for each of our 5 social login providers :
    "Authentication": {
    	"Facebook": {
    		"AppId""",
    		"AppSecret"""
    	},
    	"Google": {
    		"ClientId""",
    		"ClientSecret"""
    	},
    	"Twitter": {
    		"ConsumerKey""",
    		"ConsumerSecret"""
    	},
    	"LinkedIn": {
    		"ClientId""",
    		"ClientSecret"""
    	},
    	"Microsoft": {
    		"ClientId""",
    		"ClientSecret"""
    	}
    },
    For now we write only the skeleton of the Authentication section as we do not yet have the keys & secrets from the social login providers.

Ok with a place to store the keys & secrets of each social login provider, we can now continue to actually obtaining these keys & secrets - you should of course only follow through with the providers you are interested in.


Facebook

For Facebook we need to create a WWW application within our own Facebook account. The WWW application will allow other Facebook accounts to use Facebook to login to a specific domain - Facebook associate a specific WWW application with a specific domain. Here we will create a WWW application called bizRiot and associate it with the bizriot.com domain.

  1. Navigate your browser to https://developers.facebook.com/ and login if not already logged in.
  2. Under "My Apps" select "Add a New App".
  3. In the "Add a New App" popup select WWW.
  4. In the "Quick Start for Website" page write a name for your application (in my case bizRiot) and click on "Create New Facebook App ID.
  5. In the "Create a New App ID" popup fill in the asked 3 properties (here the application is NOT a test version) and click on "Create App ID".
  6. In the "Setup SDK page" page, we can ignore Javascript section (since we are using Nuget packages) and just fill in the Site URL.
  7. Click on the "My Apps" button to get a list of all your Facebook apps - your new application should now be listed.
  8. You can see 8 applications in my application list. The bizRiot application have a green empty circle meaning that bizRiot is NOT public available - this means that only the administrator (me) and any added testers will be able to login to bizRiot. Lets make the bizRiot application public available so everybody can login to bizRiot using their own Facebook accounts :
    1. Click on the application icon to navigate to the application.
    2. In the application page click on "App Review" menu item and then "Make bizRiot public" (you may need to supply an email address before you can do that). Other people are now allowed to login to http://bizriot.com using their own Facebook account.
  9. On the bizRiot Dashboard page we can get the App ID and the App Secret
  10. In Visual Studio open appsettings.json and copy App ID & App Secret to the Facebook section of your appsettings.json file.
  11. Lastly we must add our development domain, dev.bizriot.com, to the WWW application. Click on "Settings" in the menu and add dev.bizriot.com to "App Domains" field and then save.

Facebook have now been configured so that I can create a social login button for Facebook.

Note that it can take a couple of minutes after your Facebook WWW application have been created before it can actually be used.


Google+

The top organizational element in the Google API is called a Project. To get started with Google API it is therefore necessary to start by creating a Google API Project. Under a Google API Project you can then create Applications (holding credentials), each application may target a different platform eg. browser, iOS, Android etc. Ok, let's get started :

  1. Navigate your browser to https://console.developers.google.com/projectselector/apis/library and login if not already logged in.
  2. Create a new Google API Project (also called a google developers console project)
  3. You new project does not have any enabled APIs, for social login you need to enable the Google+ API (categorized under Social APIs).
  4. Find the Google+ API and click on it.
  5. On the Google+ API page click on the Enable button.
  6. The Google+ API is now enabled for your project (in my case the bizRiot project) and applications can now be created under the new project.
  7. Before we can actually create application credentials, we need to make an OAuth consent screen.
  8. Fill in mandatory properties (Email & Project name) and click the Save button.
  9. Back on the Credentials submenu click on the "Create Credentials" dropdown and select "OAuth client ID".
  10. On the "Credentials" page we need to select "Application type" in my case a "Web application", give the application a name in my case "bizRiot web" and to supply all the redirect urls we are going to need - I supply the following :
    • http://bizriot.com/signin-google : allow Google to let people being redirected to bizriot.com after logging in with Google.
    • http://dev.bizriot.com/signin-google : allow Google to let myself being redirect to dev.bizriot.com after logging in with Google then I develop my web application.
    • http://localhost/authorize/ : I will not guess why this is necessary, but necessary it is. Without it you will get an error.
    After filling out the "Credentials" page clik on the Create button.
  11. You will get a popup with your client ID & client secret.
  12. In Visual Studio open appsettings.json and copy client ID & client secret to the Google section of your appsettings.json file.
  13. At last I have a Web application, bizRiot web, under my bizRiot project.

Google have now been configured so that I can create a social login button for Google.


Twitter

  1. Navigate your browser to https://apps.twitter.com/ and login if not already logged in.
  2. Create a new Twitter App (existing apps are listed here).
  3. Fill in the Twitter App properties. The callback url should be http://your.domain/Account/ExternalLoginCallback - it corresponds to code we will write later in this tutorial. Accept the Terms of Use and confirm by pressing the "Create Twitter Application" button at the bottom of the page.
  4. The Twitter application is created.
  5. You can get the Consumer Key & Consumer Secret from the "Keys and Access Tokens" tab.
  6. In Visual Studio open appsettings.json and copy Consumer Key & Consumer Secret to the Twitter section of your appsettings.json file.

There is no need to configure a different callback url for your development project - it seems that Twitter will substitute the domain part of the callback url with the domain from which the twitter login request was issued - so that is nice & easy (though it also means that a single Twitter application will automatically allow ALL domains and indeed I have testet that to be true as of current).

Unfortunately to get a users email through Twitter API requires that Twitter manually whitelist our Twitter application, see more here : https://dev.twitter.com/rest/reference/get/account/verify_credentials. I have tried to ask Twitter to whitelist my application, however after 2 month I have still not received an answer.


LinkedIn

  1. Navigate your browser to https://www.linkedin.com/developer/apps and login if not already logged in.
  2. Create a new LinkedIn App (existing apps are listed here).
  3. Fill in the LinkedIn App properties. Accept the Terms of Use and click the "Submit" button.
  4. The application is now created and we can get the Client Id & Client Secret, however we still need to add 2 more properties :
    • Default Application Permissions :
      • r_basicprofile : this is added by default.
      • r_emailaddress : this we need to select.
    • Authorize Redirect URLs : (consist of a domain and the appended path signin-linkedin)
      • http://bizriot.com/signin-linkedin : for production.
      • http://dev.bizriot.com/signin-linkedin : for development.
    Finally click the "Update" button at the bottom of the page (not shown in the screenshot).
  5. In Visual Studio open appsettings.json and copy Client Id & Client Secret to the LinkedIn section of your appsettings.json file.

Your LinkedIn application is now configured to support both production & development and both email & profile picture can be retrieved.

Unfortunately the LinkedIn profile picture is watermarked with the LinkedIn logo - take a look :
(me with a LinkedIn watermark).

Microsoft

  1. Navigate your browser to https://apps.dev.microsoft.com and login if not already logged in.
  2. Create a new Microsoft Converged App (NOT a Live App) by clicking the "Add an app" button (the top not the bottom button).
  3. Fill in the app name and click the "Create application" button.
  4. The application is now created with an Application Id, however we still need to do a couple of things on this page, so don't click the Save button yet.
  5. Click on the "Generate New Password" button to create an Application Secret and immediately copy it to somewhere safe since Microsoft will not show it to you again.
  6. Add 2 Redirect Uri's one for production and one for development - these redirect uri's MUST start with https (not http), this is a new unfortunate change in Microsoft authentication service policy.
    • https://bizriot.com/signin-microsoft : for production.
    • https://dev.bizriot.com:44360/signin-microsoft : for development (for now just use the same port number as me: 44360 - later we will configure our project to use this port number).
    Finally click the "Save" button at the bottom of the page.
  7. In Visual Studio open appsettings.json and copy Application Id & Application Secret to the Microsoft section of your appsettings.json file.

Now that we are finished with configuring the social login providers and obtaining the keys & secrets from each social login provider, your appsettings.json file should now look like this :
(except the providers that you are not interested in)


Add Social Login Providers to the Login System

With a working local login system, our development environment configured and obtained keys & secrets for the social login providers, finally we can start do some actual social login programming.

  1. In Solution Explorer double click on the project.json file to open it.
  2. Add the following packages to project.json :
    "Microsoft.AspNetCore.Authentication.MicrosoftAccount""1.1.0",
    "Microsoft.AspNetCore.Authentication.Google""1.1.0",
    "Microsoft.AspNetCore.Authentication.Facebook""1.1.0",
    "Microsoft.AspNetCore.Authentication.Twitter""1.1.0",
    "Microsoft.AspNetCore.Authentication.LinkedIn""1.0.1"
  3. In Solution Explorer double click on the Start.cs file to open it.
  4. Register each social provider authentication middleware using the following code in Startup.Configure after app.UseIdentity() but before app.UseMvc(..) :
    app.UseFacebookAuthentication(new FacebookOptions()
    {
    	AppId = Configuration["Authentication:Facebook:AppId"],
    	AppSecret = Configuration["Authentication:Facebook:AppSecret"]
    });
     
    app.UseGoogleAuthentication(new GoogleOptions()
    {
    	ClientId = Configuration["Authentication:Google:ClientId"],
    	ClientSecret = Configuration["Authentication:Google:ClientSecret"]
    });
     
    app.UseTwitterAuthentication(new TwitterOptions()
    {
    	ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"],
    	ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"]
    });
     
    app.UseLinkedInAuthentication(new LinkedInOptions()
    {
    	ClientId = Configuration["Authentication:LinkedIn:ClientId"],
    	ClientSecret = Configuration["Authentication:LinkedIn:ClientSecret"]
    });
     
    app.UseMicrosoftAccountAuthentication(new MicrosoftAccountOptions()
    {
    	ClientId = Configuration["Authentication:Microsoft:ClientId"],
    	ClientSecret = Configuration["Authentication:Microsoft:ClientSecret"]
    });

    All the provider Nuget packages nicely follows the standard for how to add middleware and also each of the above extension functions will in addition add the social provider to SignInManager AuthenticationDescriptions collection. In the next section we will loop through the AuthenticationDescriptions collection to show a button for each social login.

  5. We want the UI for the social login buttons on the Login page :
    1. In Solution Explorer open the Login.cshtml file from the /Views/Account folder.
    2. To create the social login buttons we need to inject SignInManager<ApplicationUser> into the Login view - for this we need to import 2 namesapces :
      • Microsoft.AspNet.Identity : hosts SignInManager.
      • Tutorial_SocialLogin.Entities : hosts ApplicationUser.
      1. Add the 2 namespaces to the top of Login.cshtml :
        @using Microsoft.AspNetCore.Identity
        @using Tutorial_SocialLogin.Entities
      2. With the 2 namespaces we can now inject SignInManager bound to ApplicationUser (our custom domain specific extension of IdentityUser) - add the following code just below the 2 using statements :
        @inject SignInManager<ApplicationUser> SignInManager
        
    3. With SignInManager<ApplicationUser> injected we can now loop through the AuthenticationDescriptions (which we get using SignInManager.GetExternalAuthenticationSchemes()) and display a login button for each AuthenticationDescription (each social login provider) registered. Add the following code to the <!-- Social Login Section --> of Login.cshtml :
      <h4>Use another service to log in.</h4>
      <hr />
      @{
      	var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList();
      	<form asp-controller="Account" asp-action="ExternalLogin" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
      		<div>
      			<p>
      				@foreach (var provider in loginProviders)
      				{
      					<button type="submit" class="btn btn-default" name="provider" value="@provider.AuthenticationScheme" title="Log in using your @provider.DisplayName account">@provider.AuthenticationScheme</button>
      				}
      			</p>
      		</div>
      	</form>
      }
      

    Notice each form is posting to Account/ExternalLogin, the action method that will handle the click on the social login button.

  6. Coding the click on the social login buttons

    Each social login button posts back to Account/ExternalLogin - this is the place there we will send an authentication challenge request to the social login provider (which here I will like to call external authentication provider).

    1. In Solution Explorer open the AccountController.cs file from the Controllers folder.
    2. Code the ExternalLogin function - insert the following code at the bottom of the ActionController class just below POST Logout :
      // POST: /Account/ExternalLogin
      [HttpPost]
      [AllowAnonymous]
      [ValidateAntiForgeryToken]
      public IActionResult ExternalLogin(string provider, string returnUrl = null)
      {
      	// redirectUrl is the final application endpoint AFTER authentication, 
      	// redirectUrl resolves to the action method that OWIN should execute then the authentication result comes back from the external login provider.
      	var redirectUrl = Url.Action("ExternalLoginCallback""Account"new { ReturnUrl = returnUrl });
      	var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
      	return new ChallengeResult(provider, properties);
      }
      

      Then a user click one of the social login buttons, the button will post to the Account/ExternalLogin action method passing the provider name in the post collection.

      The Account/ExternalLogin method consist of the following :

      • redirectUrl : a local application url that should be the final endpoint AFTER the external login provider have authenticated (or not) the user (the external provider will actually call an OWIN endpoint in the form of http://ApplicationDomain/providerName-signin and then OWIN will call the final application endpoint here Account/ExternalLoginCallback).
      • properties : an AuthenticationProperties object that knows the provider name and the callback url (redirectUrl above).
      • ChallengeResult : returning a ChallengeResult will tell the OWIN middleware that we need authentication from an external authentication provider. We pass the provider name and the AuthenticationProperites object and OWIN will create a 302 redirect response to the browser with a redirect url to the external authentication provider API containing the relevant properties in the querystring, eg. for Facebook it may look like this :
        https://www.facebook.com/v2.2/dialog/oauth?client_id=488066541393337&scope=&response_type=code&redirect_uri=http://dev.bizriot.com/signin-facebook&state=SomeEncodedStateInformation
        • client_id : this is the App ID (funny that Facebook call it client_id here).
        • scope : I have not testet it, but it must be for extra privileges asked by the application.
        • redirect_uri : the OWIN endpoint AFTER authentication - the external authentication provider (Facebook) will redirect the browser to this url and knows nothing about the final application endpoint (the redirectUrl that we will write shortly) that will handle the application action on authentication - instead OWIN will pass control to the final application endpoint (redirectUrl Account/ExternalLoginCallback).

    3. Code the ExternalLoginCallback function - insert the following code at the bottom of the AccountController class just below the POST ExternalLogin function :
      // GET: /Account/ExternalLoginCallback
      [HttpGet]
      [AllowAnonymous]
      public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null)
      {
      	var info = await _signInManager.GetExternalLoginInfoAsync();
      	if (info == null)
      	{
      		return RedirectToAction(nameof(Login));
      	}
       
      	// Sign in the user with this external login provider if the user already has a login.
      	var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
      	if (result.Succeeded) // false the first time the user comes from the external login provider since the user does not yet have a record in the AspNetUsers table and also false if the user have already registered with another external login provider (or already registered using the local registering system)
      	{
      		if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); }
      	}
      	else
      	{
      		var email = info.Principal.FindFirstValue(ClaimTypes.Email);
      		if (string.IsNullOrEmpty(email))
      		{
      			// Typically our aplication will need the users email and as it is not for sure the external login provider will provide the users email, we better handle that by asking the users for his email if the email was not provided.
       
      			//TODO: implement no email provided
      		}
      		else
      		{
      			ApplicationUser existingUser = await _userManager.FindByEmailAsync(email);
      			if (existingUser == null)
      			{
      				// UserName is mandatory in IdentityUser, so we need to create it.
      				// If external login provider passes an email then set the UserName to the email
      				// otherwise set UserName to the users identity (ProviderKey) in the external login provider.
      				var userName = !string.IsNullOrEmpty(email) ? email : info.ProviderKey;
      				var newUser = new ApplicationUser { UserName = userName, Email = email };
      				var createResult = await _userManager.CreateAsync(newUser);
      				if (createResult.Succeeded)
      				{
      					createResult = await _userManager.AddLoginAsync(newUser, info);
      					if (createResult.Succeeded)
      					{
      						await _signInManager.SignInAsync(newUser, isPersistent: false);
       
      						if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); }
      					}
      				}
      				foreach (var error in createResult.Errors)
      				{
      					ModelState.AddModelError("User", error.Description);
      				}
      			}
      			else // email is already registered, however then registered it was not with the current external provider - it is "safe" to assume that the user currently logging in/registering also is the user that once registered with that same email : so we just overwrite the existing user with the new user.
      			{
      				// Unfortunately I cannot find out how to update the external login provider information (it does not really matter though as the user will just be logged in whatever method he uses)
       
      				await _userManager.UpdateAsync(existingUser);
       
      				await _signInManager.SignInAsync(existingUser, isPersistent: false);
       
      				if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); }
      			}
      		}
       
      		return RedirectToAction(nameof(Login));
      	}
      }
      

      So after the external login provider have authenticated (or not) the user, the external login provider will execute a function in the external provider middleware, http://YourDomain/signin-provider (eg. http://dev.bizriot.com/signin-facebook), and OWIN will then execute the above function /Account/ExternalLoginCallback.

      The external login provider middleware will create an ExternalLoginInfo object which is available on the SignInManager, so we can just ask for it. The ExternalLoginInfo contains among others LoginProvider (which is just the name of the provider) and ProviderKey (which is the users Identity on the external login provider). After we have LoginProvider & ProviderKey we can query the AspNetUserLogins table to see if the user have logged in with our website before - if he have then just log him in, if he have not then create a new ApplicationUser object for him and persist it to the Identity tables and then log him in.

  7. Ok, let's see if it works - test the social login
    1. In Visual Studio press ctrl+F5.
    2. A browser will open with the bizRiot home page (Home/Index) loaded. Click on the Login link.
    3. You should now see a button for each of the social login providers you have registered.
    4. Testing the Facebook button :
      1. Click on the Facebook button.
      2. Your browser is redirected to Facebook OAuth API asking you if you want to authorize bizRiot to access your public profile (according to documentation email should have been default provided as well, however it does currently not work not even if we add email to scope) - click on the confirm button (in my case "Continue as Rasmus").
      3. Your browser is redirect to your own web application first to the Facebook authentication middleware and then Account/ExternalLoginCallback is executed which if everything works will create you as a user, log you in and redirect to Home/Index. Notice that in this case Facebook OAuth API did not pass the users email address (more on that below) - you can see the UserName is instead made up of the users Identity on Facebook.
      4. Notice that if you log off and then try to login with Facebook again, Facebook does not require you to go through the authorization process again - Facebook already know that you have authorized bizRiot to access your public profile and immediately redirect your browser back to bizRiot.
    5. Testing the Google button :
      1. Click on the Google button.
      2. Your browser is redirected to Google OAuth API which shows you what bizRiot is asking to access - click on the Allow button.
      3. Your browser is redirect to your own web application first to the Google authentication middleware and then Account/ExternalLoginCallback is executed which if everything works will create you as a user, log you in and redirect to Home/Index.
      4. Notice that if you log off and then try to login with Goole again, Google does not require you to go throught the authorization process again.
    6. Testing the Twitter button :
      1. Click on the Twitter button.
      2. Your browser is redirected to Twitter OAuth API which shows you what bizRiot is asking to access - click on the Authorize app button.
      3. Your browser is redirected back to the login page and you are NOT logged in. The reason is because Twitter default does NOT pass an email and our code specifically does not yet handle the case that the external authentication provider does not pass an email address - however we will handle this case in moment (and Twitter login will then work).

        Note that we could indeed have written the code to allow creation of the user without an email, in which case the username returned by Twitter would have been a user identifier number like this
    7. Testing the LinkedIn button :
      1. Click on the LinkedIn button.
      2. Your browser is redirected to LinkedIn OAuth API which shows you what bizRiot is asking to access - click on the Allow Access button.
      3. Your browser is redirect to your own web application first to the Google authentication middleware and then Account/ExternalLoginCallback is executed which if everything works will create you as a user, log you in and redirect to Home/Index.
      4. Notice that if you log off and then try to login with LinkedIn again, LinkedIn does not require you to go throught the authorization process again.
    8. Testing the Microsoft button :

      Unfortunately Microsoft have just changed their social authentication service policy - you can now ONLY use their authentication service if you use SSL. Luckily Visual Studio makes it easy to test SSL :

      1. In Visual Studio Solution Explorer right click on the project name and select properties.
      2. In Project Properties select the Debug tab.
      3. Change the App URL to https://dev.bizriot.com:44360 and select the SSL option.

        It is important that the port number is the same as then you registered the Microsoft App Redirect URI. If Visual Studio gives you a different port number, you need to open launchSettings.json and change the port number there : this will automatically change the port number in your project properties page:

        1. In Solution Explorer open launchSettings.json from the Properties folder.
        2. Change the port number to 44360.

      Ok with Visual Studio automatically creating a self signed certificate for you, we can start testing the Microsoft login button :

      1. Press ctrl+F5 to compile and load a browser tab with the new App Url.
      2. Click on the Microsoft button.
      3. Your browser is redirected to Microsoft OAuth API which shows you what bizRiot is asking to access - click on the Yes button.
      4. Your browser is redirect to your own web application first to the Microsoft authentication middleware and then Account/ExternalLoginCallback is executed which if everything works will create you as a user, log you in and redirect to Home/Index.
      5. Notice that if you log off and then try to login with Microsoft again, Microsoft does not require you to go throught the authorization process again.

      Don't forget to remove SSL setting for you project after testing the Microsoft login button:

      1. In Visual Studio Solution Explorer right click on the project name and select properties.
      2. In Project Properties select the Debug tab.
      3. Change the App URL back to http://dev.bizriot.com and de-select the SSL option.

    So far so god - the tests were successful except that our code does not yet handle the Twitter case (Twitter does not provide the users email address and our code cannot yet handle that situation).

  8. Examining the database Identity tables

    After we have tested the social login providers, we will have some content in the Identity tables. The 2 most relevant tables are :

    • AspNetUserLogins : external login providers connected to application users.
      Relevant columns
      • LoginProvider : the identifying name of the external login provider.
      • ProviderKey : the users identifier on the login provider (eg. the users ID on Facebook).
      • UserId : the users local identifier on the application - AspNetUserLogins.UserId = AspNetUsers.Id.
    • AspNetUsers : application users.
      Relevant columns
      • Id : the users identifier on the application - AspNetUserLogins.UserId = AspNetUsers.Id.
      • Email : the users email if any.
      • NormalizedUserName : the usename in uppercase.
      • PasswordHash : here we can see that we do NOT store a password for the users that logs in with an external login provider.
    • From the tables you can see that the rasmus@favouritedesign.com user is not linked to any external login provider as the rasmus@favouritedesign.com user was registered locally and therefore also has a PasswordHash.


  9. Enforcing an email address from the user

    Most applications that allows users to register will need to have an email address on the users and since sometimes (or even often) the external login provider will NOT provide an email address, we need to ask the user to provide us with an email if the external login provider have not done so.

    Enforcing signing up with an email address also minimize the problem if users are using different social login buttons because we can test if the user exists on the email address instead of on the provider name & provider key.

    Ok, so in case we could not obtain the email addres from the external login provider, we need to ask the user for the email address in the signup process - to do that we need to create yet another page, let's call it ExternalLoginConfirmation and as usual we need :

    • The ViewModel : ViewModels\Account\ExternalLoginConfirmationViewModel.cs
    • The View : Views\Account\ExternalLoginConfirmation.cshtml.
    • The Controller Action : Account/ExternalLoginConfirmation.

    1. Create the ViewModel - ExternalLoginConfirmationViewModel :
      1. Create a new file ExternalLoginConfirmationViewModel.cs in the ViewModels\Account folder.
      2. Add the following 4 properties to ExternalLoginConfirmViewModel :
        [Required]
        [EmailAddress]
        public string Email { getset; }
         
        public string NameIdentifier { getset; }
         
        public string ProfilePictureUrl { getset; }
         
        public string LoginProvider { getset; }
        

      Notice the NameIdentifier, which is the users identifier on the social login provider (here called NameIdentifier but in the AspNetUserLogins table is called ProviderKey).

    2. Create the View - ExternalLoginConfirmation.cshtml :
      1. Create a new file ExternalLoginConfirmation.cshtml in the Views\Account folder.
      2. Remove the template code and insert the following code instead :
        @model Tutorial_SocialLogin.ViewModels.Account.ExternalLoginConfirmationViewModel
         
        <h2>Register</h2>
         
        <form asp-controller="Account" asp-action="ExternalLoginConfirmation" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" class="form-horizontal" role="form">
        	<input type="hidden" asp-for="LoginProvider" value="@Model.LoginProvider" />
        	<input type="hidden" asp-for="ProfilePictureUrl" value="@Model.ProfilePictureUrl" />
         
        	<p class="text-info">
        		You've successfully authenticated with <strong>@Model.LoginProvider</strong>.
        		Please enter your email address and click the Register button to finish
        		logging in.
        	</p>
         
        	<div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
         
        	<div class="form-group">
        		<div class="col-md-10 col-md-offset-2">
        			<img src="@Model.ProfilePictureUrl" />
        		</div>
        	</div>
        	<div class="form-group">
        		<label asp-form="Email" class="col-md-2 control-label"></label>
        		<div class="col-md-10">
        			<input asp-for="Email" value="@Model.Email" class="form-control" />
        			<span asp-validation-for="Email" class="text-danger"></span>
        		</div>
        	</div>
        	<div class="form-group">
        		<div class="col-md-offset-2 col-md-10">
        			<button type="submit" class="btn btn-default">Register</button>
        		</div>
        	</div>
        </form>
         
        @section Scripts {
        	@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
        }
    3. Create the Controller Action - ExternalLoginConfirmation :
      1. Open the AccountController.cs file from the Controllers folder.
      2. At the bottom of the AccountController class just below GET ExternalLoginCallback method add the following Action method :
        // POST: /Account/ExternalLoginConfirmation
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
        {
        	if (_signInManager.IsSignedIn(User))
        	{
        		return RedirectToAction(nameof(HomeController.Index), "Home");
        	}
         
        	if (ModelState.IsValid)
        	{
        		var info = await _signInManager.GetExternalLoginInfoAsync();
        		if (info == null// this should never happen since if ExternalLoginInfo is not available ExternalLoginConfirmation cannot be reached.
        		{
        			ModelState.AddModelError("""Unexplained error");
        			return View(model);
        		}
         
        		var newUser = new ApplicationUser { UserName = model.Email, Email = model.Email, ProfilePictureUrl = model.ProfilePictureUrl };
        		var result = await _userManager.CreateAsync(newUser); // will fail if email already exists
        		if (result.Succeeded)
        		{
        			result = await _userManager.AddLoginAsync(newUser, info);
        			if (result.Succeeded)
        			{
        				await _signInManager.SignInAsync(newUser, isPersistent: false);
        				if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); }
        			}
        		}
        		foreach (var error in result.Errors)
        		{
        			ModelState.AddModelError("Email", error.Description);
        		}
        	}
         
        	ViewData["ReturnUrl"] = returnUrl;
        	return View(model);
        }

        ExternalLoginConfirmation basic responsibility is to force the user to write an email address. ExternalLoginConfirmationViewModel.Email is marked Required and ExternalLoginConfirmation returns the view again if ModelState is not valid.

        Also notice that in ExternalLoginConfirmation we do NOT allow registering with an email address already existing - var result = await _userManager.CreateAsync(newUser); will fail if the email already exists and provide a validation error. The reason not to just login the user if the email address already exists is because we have not forced the user to confirm that email address to avoid the user to be able to take over the account of another user just by logging in using an external login provider and then write a fake email address.

    4. Redirect to ExternalLoginConfirmation upon registering with external login provider :

      We also need to change Account/ExternalLoginCallback to send the user to ExternalLoginConfirmation if the external login provider does not pass an email address and if any problem occurs we also want to send the user to ExternLLoginConfirmation.

      1. In the AccountController.cs file find the ExternalLoginCallback function (it should be just above the ExternalLoginConfirmation function) and add the following 3 pieces of code :
        • Just below the var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email); insert the following code :
          var nameIdentifier = info.ExternalPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
        • Instead of the //TODO: implement no email provided insert the following :
          return View("ExternalLoginConfirmation"new ExternalLoginConfirmationViewModel { Email = email, NameIdentifier = nameIdentifier, LoginProvider = info.LoginProvider });
          

          The responsiblity of this code piece is to return the ExternalLoginConfirmation view if the social login provider did not pass an email address.

          Again also notice the nameIdentifier (users identifier on the social login provider) which we can obtain in 2 different ways :

          • var nameIdentifier = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
          • var nameIdentifier = info.ProviderKey;

          In the database we store nameIdentifier as AspNetUserLogins.ProviderKey.

        • Just below the foreach (var error in createResult.Errors) {..} section insert the following :
          if (ModelState.ErrorCount > 0)
          {
          	return View("ExternalLoginConfirmation"new ExternalLoginConfirmationViewModel { Email = email, NameIdentifier = nameIdentifier, LoginProvider = info.LoginProvider });
          }

  10. Ok, let's test a situation there the external login provider does NOT pass an email address - in our case that will be Twitter
    1. In Visual Studio press ctrl+F5 to compile and open a browser tab on the application url.
    2. Click on the Login link to navigate your browser to the login page.
    3. Click on the Twitter button
    4. Voila! your browser is served the ExternalLoginConfirmation page.
    5. Insert an email address you have already used, in my case rasmus@webmodelling.com and click the Register button - you get a validation message that this email is already in use.
    6. Insert an email address you have not used yet, eg. for@test.only and click the Register button - success !

The basic technical foundation of social login is now covered, however based on access to the social login providers Graph API's there are many possible enhancements - in the next section we will explore how to get the users profile picture url.



Get profile picture from social login providers

Then making a web application allowing users to login, we are often interested in using profile pictures as visual identification of users. Instead of trying to get the users to upload a picture of themselves, it is more easy for the user (and more reliable for the web application) if we can automatically fetch profile pictures from the social login providers as part of the registration process.

Unfortunately support for retrieving profile pictures differs wildly between social login providers :

  • Facebook : easy to get the profile picture url.
  • Google : less easy but possible.
  • Twitter : manual review of our application by Twitter staff required - good luck with that.
  • LinkedIn : no support in newest RTM 1.0.1 Nuget and in addition the profile picture is watermarked with a small LinkedIn icon.
  • Microsoft : not possible.

On the code level we store the profile picture url in ApplicationUser.ProfilePictureUrl and most of our code is already prepared to handle it, however not all the code, especially AccountController.ExternalLoginCallback as well as the PartialLogin view need to be updated to handle the profile picture url.

  • AccountController.ExternalLoginCallback : is the final endpoint in the OAuth communication with the social login providers and the place there we create the ApplicationUser and therefore also the place there we need to either already have or get the users profile picture url.
  • Views\Shared\PartialLogin : is displayed on all pages and the natural place to display a profile image of the logged in user.
  1. In AccountController.ExternalLoginCallback we need to amend ProfilePictureUrl to the instantiations of ApplicationUser & ExternalLoginConfirmationViewModel :
    1. In Visual Studio Solution Explorer open the AccountController.cs file.
    2. Find the AccountController.ExternalLoginCallback function and add/change the following :
      1. Just below var nameIdentifier = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier); insert the following :
        var profilePictureUrl = "";
        
      2. Change (2 places - see the screenshot for 2A & 2B)
        return View("ExternalLoginConfirmation"new ExternalLoginConfirmationViewModel { Email = email, NameIdentifier = nameIdentifier, LoginProvider = info.LoginProvider });
        
        to
        return View("ExternalLoginConfirmation"new ExternalLoginConfirmationViewModel { Email = email, NameIdentifier = nameIdentifier, LoginProvider = info.LoginProvider, ProfilePictureUrl = profilePictureUrl });
        
      3. Change
        var newUser = new ApplicationUser { UserName = userName, Email = email };
        
        to
        var newUser = new ApplicationUser { UserName = userName, Email = email, ProfilePictureUrl = profilePictureUrl };
        
  2. In PartialLogin we currently show the logged in users email address, but instead of the email address we want to show the users profile picture :

    We need a way to get the ApplicationUser.ProfilePictureUrl and since ProfilePictureUrl is a custom property on our IdentityUser extension, ApplicationUser, the ClaimsPrincipal user available in the PartialLogin View does NOT automatically contain the ProfilePictureUrl property. However since ClaimsPrincipal is available as RazorPage.User on any View, we can make ProfilePictureUrl available in any razor View by writing an extension function, ProfilePictureUrlAsync (let's make it async), on the ClaimsPrincipal.

    1. Add a ProfilePictureAsync extension function to the ClaimsPrincipal class :
      1. Solution Explorer create a new folder structure under the project root :
        • Utils
          • Extensions
      2. In the Utils\Extensions folder create a new file called ClaimsPrincipalExtensions.cs.
      3. Change the ClaimsPrincipalExtensions class to static.
      4. Insert the ProfilePictureAsync extension function code :
        public static async Task<string> ProfilePictureUrlAsync(this ClaimsPrincipal user, UserManager<ApplicationUser> userManager)
        {
        	string profilePictureUrl = "/images/defaultProfilePicture.png";
         
        	if (user.Identity.IsAuthenticated)
        	{
        		ApplicationUser appUser = await userManager.FindByIdAsync(userManager.GetUserId(user));
         
        		if (appUser != null && !String.IsNullOrEmpty(appUser.ProfilePictureUrl))
        		{
        			profilePictureUrl = appUser.ProfilePictureUrl;
        		}
        	}
         
        	return profilePictureUrl;
        }

        First we set profilePictureUrl to a default profile picture and if the user is logged in and also has an actual profile picture, then we overwrite the profilePictureUrl to use the users profile picture instead of the default profile picture.

        The ProfilePictureUrlAsync extension function needs the ApplicationUser bound UserManager, UserManager<ApplicationUser>, to locate the ApplicationUser with the same ID as the ClaimsPrincipal UserID :

        ApplicationUser appUser = await userManager.FindByIdAsync(userManager.GetUserId(user));
        
    2. In Solution Explorer open the PartialLogin.cshtml file from the Views\Shared folder.
    3. In PartialLogin.cshtml do the following :
      1. Insert the following using statement
        @using Tutorial_SocialLogin.Utils.Extensions
        
      2. Delete the LI tag containing the users email address
        <li>
        	<a href="#">Hello @UserManager.GetUserName(User)</a>
        </li>
      3. Add an IMG element just below the UL end-tag
        <img title="@UserManager.GetUserName(User)" src="@await User.ProfilePictureUrlAsync(UserManager)" style="width:80px;position:absolute;right:200px;" />
        

        RazorPage.User is a ClaimsPrincipal, so we can directly use the ClaimsPrincipal ProfilePictureUrlAsync extension function in any RazorPage View like User.ProfilePictureUrlAsync - nice.

    4. Setup the default profile picture
      1. In Solution Explorer create a new sub-folder called images in the wwwroot folder.
      2. Copy a default profile picture to the wwwroot\images folder. If you don't have any, you can use this : default profile picture
  3. Retrieving the ProfilePictureUrl from the external authentication providers :

    Important : Delete all Users created so far :

    1. Open SQL Server Object Explorer and expand Tutorial_SocialLogin tables node
    2. Right click on the AspNetUsers table and select View Data from the shortcut menu
    3. Select all records and delete them

    In the AccountController.ExternalLoginCallback function the profilePictureUrl variable is currently empty - however for each social login provider that supports it, we will create a profilePictureUrl value.

    • Facebook

      Facebook makes it easy to retrieve a users profile picture url, all you need to know is the url format and the users facebook identity (in the code called nameIdentifier).

      1. Open AccountController.cs from Solution Explorer.
      2. In the AccountController.LoginExternalCallback function just below var profilePictureUrl = ""; insert the following code snippet :
        if (info.LoginProvider.ToLower() == "facebook")
        {
        	profilePictureUrl = "http://graph.facebook.com/" + nameIdentifier + "/picture";
        }
        
      3. Test Facebook profile picture url
        1. In Visual Studio press Ctrl+F5 to compile and load a browser on the application url.
        2. Click on the Login link to navigate to the login page.
        3. Click on the Facebook button - your browser will redirect to Facebook OAuth API which call into the Facebook Nuget middleware which execute AccountController.ExternalLoginCallback and because Facebook pass the users email, ExternalLoginCallback will create an ApplicationUser, log you in and redirect your browser once again this time to the default page.
        4. You can now see the profile picture in the header.

    • Google

      The Google Nuget package does not directly have the url to the users profile picture, however if we have a userid Google allows us to get some basic properties (including a profile picture url) of that user as a json response to a url request.

      1. In Solution Explorer create a new sub-folder, ExternalData under the Utils folder.
      2. Create a new file, GoogleProfilePicture.cs, in the Utils\ExternalData folder.
      3. Add a function GetGoogleProfilePictureUrlAsync to the GoogleProfilePicture class :
        public static async Task<string> GetGoogleProfilePictureUrlAsync(string googleUserId)
        {
        	string getGoogleUserUrl = "http://picasaweb.google.com/data/entry/api/user/" + googleUserId + "?alt=json";
         
        	try
        	{
        		using (var client = new HttpClient())
        		{
        			client.DefaultRequestHeaders.Accept.Clear();
         
        			HttpResponseMessage response = await client.GetAsync(getGoogleUserUrl);
        			if (response.IsSuccessStatusCode)
        			{
        				string googleUserJson = await response.Content.ReadAsStringAsync();
        				dynamic googleUser = JsonConvert.DeserializeObject(googleUserJson);
        				string googleProfilePictureUrl = googleUser["entry"]["gphoto$thumbnail"]["$t"].Value;
         
        				return googleProfilePictureUrl;
        			}
        		}
        	}
        	catch (Exception ex)
        	{
        		// Log this error, but let the login process succeed
        	}
         
        	return "";
        }
        
      4. In Solution Explorer open the AccountController.cs file from the Controllers folder.
      5. In AccountController.ExternalLoginCallback just below the if (info.LoginProvider.ToLower() == "facebook") {..} section add the following code :
        else if (info.LoginProvider.ToLower() == "google")
        {
        	profilePictureUrl = await Utils.ExternalData.GoogleProfilePicture.GetGoogleProfilePictureUrlAsync(nameIdentifier);
        }
        
      6. Test Google profile picture url
        1. In Visual Studio press Ctrl+F5 to compile and load a browser on the application url.
        2. Click on the Login link to navigate to the login page.
        3. Click on the Google button - your browser will redirect to Google OAuth API which call into the Google Nuget middleware which execute AccountController.ExternalLoginCallback and because Google pass the users email, ExternalLoginCallback will create an ApplicationUser, log you in and redirect your browser once again this time to the default page.
        4. You can now see the profile picture in the header.
    • Twitter

      A Twitter application needs to be manually reviewed for approval to retrieving a users profile picture from the Twitter API - I have contacted Twitter for my own bizRiot application, however after 2 month they have still not responded.

    • LinkedIn

      Unfortunately the new RTM LinkedIn Nuget package seems NOT to support retrieving the profile picture (the old one for RC1 did). A solution would be to use the generic OAuth package instead and write the communication with the LinkedIn API yourself (there are plenty of tutorials on how to do that), however since the LinkedIn profile picture retrievable from the API is watermarked with the LinkedIn logo, the benefit of using LinkedIn for profile picture is mediocre at best and so I will not show how to do it here.

      Here is how the LinkedIn API retrieved profile picture looks like.

    • Microsoft

      It is currently NOT possible to retrieve the profile picture url from Microsoft Graph API

The different social login providers do not provide exactly the same developer nor enduser experience :

  • Facebook will not pass the email address if the users email is not public, however profile picture url is available and the newest Facebook Nuget package is very easy to use (RTM improves over RC1 that email can now be retrieved without writing a BackChannalHandler).
  • Google is less easy to use and some JSON parsing have to be written to get the profile picture.
  • Microsoft requires an SSL conection and does NOT support profile picture retrieval (RTM improves over RC1 that email is now default passed).
  • Twitter shows a message everytime the user logs in even though the user does not need to authorize anything. In addition no profile picture is available and also Twitter will not pass the email address unless Twitter manually wet your application.
  • LinkedIn does not anymore provide a profile picture url in the LinkedIn Nuget package and if you choose to use the OAuth package and write the API communication yourself, the LinkedIn profile picture would be watermarked anyway (RTM is worse than RC1 in that there is no url available for the profile picture).

Currently the best working social login providers are Facebook & Google (even though you cannot reliably get the email from Facebook and you have to write extra code to get the profile picture from Google), however for the basic authorization service all the major social login providers are stable and work pretty much the same except maybe for Twitter that is a pain with the authorization message.

This tutorial have covered the main social login providers : Facebook, Google, Microsoft, Twitter & LinkedIn, however there are a lot more social login providers - you can find Nuget packages for many of them here : https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/tree/dev/src



Appendix : Common errors and solutions

  1. Can't Load URL: The domain of this URL isn't included in the app's domains.

Reason : This error happens then trying to login with Facebook and the redirect_uri parameter in the Facebook login request url have NOT been registered as a WWW application with Facebook.

Solution : If your website runs on localhost:6789 then you MUST register localhost:6789 as a WWW application with Facebook. If your website runs on webmodelling.com then you MUST register webmodelling.com as a WWW application with Facebook.
To check your app domain :

  • In Visual Studio
    1. In Visual Studio Solution Explorer right-click on the project node and select Properties from the shortcut menu.
    2. Check or change your app url.
  • In Facebook App
    1. Login to the Facebook developer page and select your application.
    2. In Settings for the selected application you can see App Domans and Site Url - one of these MUST match the Visual Studio App URL (for App Domains the Facebook value only need to match the domain (and port if any) part of the Visual Studio App URL)

Note : You may also get this error if you try to login just after you created the Facebook WWW application because it takes a couple of minutes before the WWW application is ready.

  1. Error : redirect_uri_mismatch

Reason : This error happens then trying to login with Google and the list of authorized redirect urls in the Google application does NOT contain http://localhost/authorize

Solution : Add http://localhost/authroize to the list of authorized redirect urls in your Google application.

  1. Error : deleted_client

Reason : This error happens then trying to login with Google using a client ID that have been deleted.

Solution : Be sure to use the client ID and client secret of an existing Google application, eg. if you have deleted a Google application and recreated it, the client ID & secret for this application name will have changed and you must update your code with the new client ID & secret.

  1. HttpRequestException by System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() Response status code does not indicate success: 401 (Authorization Required)

Reason 1 : This error happens then trying to login with Twitter using a wrong ConsumerKey or a wrong ConsumerSecret.

Reason 2 : Your Twitter application does not contain any Callback URL.

Solution 1 : Be sure ConsumerKey & ConsumerSecret are both correct.

Solution 2 :

Fill in an appropriate Callback URL in your Twitter application. The Callback URL is an url pointing to the code in your website that Twitter should redirect to after logging in the user.

  1. invalid redirect_uri

Reason : This error happens then trying to login with LinkedIn and the application identified by the API Key you supply does not contain a redirect_uri with the domain from there the login request was send.

Example : Eg. if you try to use LinkedIn to login from http://dev.bizriot.com, the redirect_uri sent to LinkedIn API will be http://dev.bizriot.com/signin-linkedin and if the LinkedIn application (selected by the API Key sent in the login request) does not have that url in the list of Authorized Redirect URLs, then you will get the invalid redirect_uri error.

Solution :

Add the relevant redirect url to your applications Authorized Redirect URLs (remember to click the "Update" button at the bottom of the page not shown in the screenshot).

  1. Error : invalid_request
    Error description : The provided value for the input parameter 'redirect_uri' is not valid. The expected value is 'https://login.live.com/oauth20_desktop.srf' or a URL which matches the redirect URI registered for this client application.

Reason : This error happens then trying to login with Microsoft without correct

Solution : Add http://localhost/authorize to the list of authorized redirect urls in your Google application.

  1. System.TypeLoadException: Method 'get_Configuration' in type 'Microsoft.Data.Entity.Design.Internal.HostingEnvironment' from assembly 'EntityFramework.Commands, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60' does not have an implementation.

Reason : This error happens then trying to use dnx ef to create a migration but some of the dependencies (Nuget packages) in use are newer than DNX, eg. in my case my DNX was RC1 but some packages were RC2.

  • shell> dnvm list : shows you the version of Dot Net Execution Environment that your project is running on.
  • shell> dnx : shows you the version of DNX utility that you have installed.


Comments

You can comment without logging in
 
 B  U  I  S 
Words: Chars: Chars left: 
 Captcha 
 Nickname
Facebook
    


click to top