Click here to Skip to main content
15,867,141 members
Articles / Web Development / ASP.NET / ASP.NET Core

ASP.NET Core Road to Microservices Part 03: Identity

Rate me:
Please Sign up or sign in to vote.
4.95/5 (21 votes)
30 Jun 2021CPOL26 min read 50.8K   797   48   19
Implementing ASP.NET Core Identity so our e-Commerce can benefit from authentication, authorization and user identification

Identity

Article Series

Introduction

At the end of part 2 of this sequence of articles, we had an e-commerce application with views in which the user could choose products from a catalog, place them in the shopping basket and fill in a registration form with address and other personal data for future shipping procedures. Of course, all of it was made using dummy data.

Our application does not currently require user login or password of any kind. A growing number of e-commerce websites also choose not to require this type of information, requesting only the customer's credit card or other payment methods at the checkout page. On the other hand, many e-commerce websites require a login and password to authenticate the user.

Both models have benefits and drawbacks. An e-commerce website that does not require authentication is more convenient for customers as it reduces friction that could hurt conversions, in user experience parlance. On the other hand, authentication enables you to identify users and possibly better analyze their behavior over time, as well as allowing you to provide users with certain benefits, such as displaying an order history of a customer that has previously purchased on the website. In this article, we will follow the second approach.

In this third installment of the article series, we will use a login system and ensure that our application is accessed only by authenticated users. It allows you to protect the sensitive points of the application from anonymous users. With authentication, we ensure the user enters the system through a secure identification service. That also enables the application to track user access, identify usage patterns, automatically fill out registration forms, view customer order history, and other conveniences, enhancing user experience.

If all you need is a user table with login and password columns and a user profile for your application, then ASP.NET Core Identity is the best option for you.

In this chapter, we will learn how to install ASP.NET Core Identity in our e-commerce solution and take advantage of the security, login/logout, authentication, and user profile features provided by this framework.

By default, the database engine used by Identity is SQL Server. However, we will be using SQLite, which is a simpler and more compact database engine than SQL Server. Before installing Identity, we will prepare the project to use this new database engine.

Right-click the MVC project name, choose the Add NuGet Package submenu, and the package installation page opens, enter the package name: Microsoft.EntityFrameworkCore.SQLite.

Add Nuget Package

Picture: The project context menu

Sqlite

Picture: Adding the Microsoft.EntityFrameworkCore.Sqlite Package

Now click the "Install" button and wait for the package to install.

Okay, now the project is ready to receive the ASP.NET Core Identity scaffolding.

Installing ASP.NET Core Identity

Applying the ASP.NET Core Identity Scaffold

Scaffolding

Installing a new ASP.NET Core with Identity from the beginning is different from installing it in an existing project. Since our project does not have Identity, we will install a package of files and assemblies containing the functionalities we need. This process is similar to building walls in a construction site using prefabricated modules. This process is known as scaffolding.

If we had to manually create login/logout, authentication and other features in our application, that would require a lot of effort. We are talking about the development of views, business logic, model entities, data access, security, etc., in addition to many hours of unit testing, functional testing, integrated testing and so on.

Fortunately, our application can benefit from authentication and authorization features without much effort. Authentication and authorization are ubiquitous in web applications. Because of this, Microsoft provides a package that can be transparently installed in ASP.NET Core projects that lack such features. It's called ASP.NET Core Identity.

To apply ASP.NET Core Identity in our solution, we right-click the project, click Add Scaffolded Item and then choose the Add option. That will open a new Add Scaffold dialog window.

New Scaffolding Item

Picture: The Project Context Menu

Here, we will choose Installed > Identity > Identity.

Add Scaffold

Picture: The Add Scaffold Dialog

The ASP.NET Core Identity Scaffold will open a new dialog window containing a series of configuration parameters. There, you can define the layout of the pages, what source code you will include, the data and user context classes, and also which type of database (SQL Server or SQLite) Identity will use.

Add Identity

Picture: The Add Identity Dialog

Let's select these options:

  • Layout: The _Layout.cshtml file that already exists in our project. It will define a basic markup to be shared by the Identity pages and the rest of our application.
  • Identity pages: Login, Logout, Register, ExternalLogin. The scaffolding process will copy those pages to our application, where you can edit them. Note that you still can navigate to the other Identity pages that you left unmarked, but you cannot modify or customize them since they will not be present in the project.
  • Context class: AppIdentityContext.
  • User class: AppIdentityUser. Represents a user in the identity system

After confirming these parameters, the scaffolding will modify our project. The most notable change is the new file structure under the Areas / Identity folder of our project.

Identity Area

Picture: The Areas/Identity Project Folder

Observe the new structure under Areas folder:

  • The AppIdentityContext class: This is the class used for the Entity Framework database context for ASP.NET Core Identity.
  • The AppIdentityUser class: represents a user in the identity system.
  • The pages below Pages / Account: Those are pages containing the markup code for Identity pages. They are Razor Pages, that is, a kind of MVC structure type where the view is in the file and the actions of the controller and the template reside in a single file. As we have said, these pages can be modified and customized in our application, but the other Identity pages can be accessed, but not changed, since their files are not present in the project.
  • Partial Views: _ValidationScriptPartial, _ViewImports, _ViewStart
  • IdentityHostingStartup class: The ASP.NET Core WebHost executes this class as soon as the application runs. The IdentityHostingStartup class configures database and other services that Identity needs to work.

Creating and Applying ASP.NET Core Identity Model Migration

It is not enough to install the ASP.NET Core Identity package in our project; we still have to generate the database schema, which includes tables and initial data required by for ASP.NET Identity Core.

When we made the scaffolding of ASP.NET Identity Core, a new Identity data model was automatically added to our project, as we can see in the IdentityHostingStartup.cs file class:

Razor
public void Configure(IWebHostBuilder builder)
{
    builder.ConfigureServices((context, services) => {
        services.AddDbContext<AppIdentityContext>(options =>
            options.UseSqlite(
                context.Configuration.GetConnectionString("AppIdentityContextConnection")));

        services.AddDefaultIdentity<AppIdentityUser>()
            .AddEntityFrameworkStores<AppIdentityContext>();
    });
}
Listing: Configuring Identity at the IdentityHostingStartup.cs file

Note how the above Entity Framework configuration (AddDbContext method) is using the AppIdentityContext class, a name we chose in the scaffolding process.

The same process also added a new AppIdentityContextConnection connection string to the appsettings.json configuration file. ASP.NET Core Identity will use this connection string to access the SQLite database:

JavaScript
.
.
.
"AllowedHosts": "*",
"ConnectionStrings": {
    "AppIdentityContextConnection": "DataSource=MVC.db"
}
Listing: Configuring SQLite Connection at the appsettings.json

But note that the scaffolding process alone did not create the Identity SQLite database by itself. This can be achieved by creating a new Entity Framework Migration.

To add a new migration, open the Tools > Package Manager Console menu, and type in the console.

PM> Add-Migration Identity 

The above command added the classes containing the migration statements, but it did not create the database itself:

Add Migration

Picture: The Migration Project Folder

In order to create the SQLite database, you must apply the migration by executing the Update-Database command:

PM> Update-Database -verbose

This command creates the MVC.db database file defined in the connection string included in the appsettings.json configuration file:

Mvc Db

Picture: The SQLite Database File

Now let's take a look at this file by double-clicking on it. This will open the DB Browser for SQLite application we installed at the beginning of this article:

Db Browser 1

Picture: The DB Browser for SQLite Tool

That's it! Now our application already has all the necessary components to perform authentication and authorization. From now on, we will start using these components to integrate ASP.NET Core Identity features in our application.

Configuring ASP.NET Core Identity

Adding Identity Components to the Back-End

Backend

The Identity components are already present in our project. However, we need to add further configuration that will integrate these components with the rest of the application.

In software architecture, that is referred to as middleware.

ASP.NET Core provides a standard approach to integrate a middleware into the normal execution of the application. This mechanism resembles a water pipeline. Each new service further extends the plumbing system, taking the water at one end, and passing it to the next segment.

P I P E L I N E

Picture: ASP.NET Core Pipeline

Similarly, ASP.NET Core will pass requests along a chain of middlewares. Upon receiving a request, each middleware decides either to process it or to pass the request to the next middleware in the chain. If the user is anonymous and the resource requires an authorization, then Identity will redirect the user to the login page.

The scaffolding process created the IdentityHostingStartup class, which already configured some Identity services.

C#
public void Configure(IWebHostBuilder builder)
{
    ...
        services.AddDefaultIdentity<AppIdentityUser>()
            .AddEntityFrameworkStores<AppIdentityContext>();
    ...
}
Listing: The IdentityHostingStartup class

The AddDefaultIdentity() method adds a set of common identity services to the application, including a default UI, token providers, and configures authentication to use identity cookies.

Identity is enabled by calling the UseAuthentication() extension method. This method adds authentication middleware to the request pipeline:

C#
...
app.UseStaticFiles();
app.UseAuthentication();
...
Listing: Including Identity to the ASP.NET Core pipeline

The UseAuthentication() method adds the authentication middleware to the specified ApplicationBuilder, which enables authentication capabilities.

However, the above code configures just the back end behavior. For the front end, you can integrate ASP.NET Core Identity views with the application user interface by including a partial view in the layout markup that will allow users to log in or register. Let's take a look at it in the next section.

Adding Identity Components to the Front-End

Frontend

The ASP.NET Core Identity scaffolding process includes the LoginPartial file in the Views\Shared folder. This file contains the partial view that displays either the authenticated user's name or hyperlinks for login and registration.

File Loginpartial

Picture: The _LoginPartial.cshtml Partial View
Razor
@using Microsoft.AspNetCore.Identity
@using MVC.Areas.Identity.Data
@inject SignInManager<AppIdentityUser> SignInManager
@inject UserManager<AppIdentityUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" 
         asp-page="/Account/Manage/Index" 
         title="Manage">Hello @UserManager.GetUserName(User)!</a>
    </li>
    <li class="nav-item">
        <form id="logoutForm" class="form-inline" asp-area="Identity" 
         asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action
         ("Index", "Home", new { area = "" })">
            <button id="logout" type="submit" class="nav-link btn btn-link text-dark">
             Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" id="register" asp-area="Identity" 
         asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" id="login" asp-area="Identity" 
         asp-page="/Account/Login">Login</a>
    </li>
}
</ul>
Listing: The _LoginPartial.cshtml partial view

You can add this component to any of the application views, with the line below:

HTML
<partial name="_LoginPartial" />

However, adding this line more than one time would cause undesirable code duplication. We can avoid this redundancy by including the line above in the standard layout view of the application (_Layout.cshtml file) since this will cause the component to be visible through all of our e-commerce views. We need to include it more specifically in the application's navigation bar, inside the element that contains the "navbar-collapse" class:

Razor
<div class="navbar-collapse collapse justify-content-end">
    <partial name="_LoginPartial" />
    <ul class="nav navbar-nav">
Listing: Including the _LoginPartial partial view in the Layout.cshtml page

By running the application, we can now see the log and login links at the upper right corner of the product search page:

Register Link

Now we will click to add any product to navigate to the shopping cart page. Note how the login and register links are also present here:

Register Link

Razor Pages

When you install ASP.NET Core Identity scaffolding, the new Identity components included in your project do not follow the MVC architecture. Instead, Identity components are based on Razor Pages.

But what's the difference between MVC and Razor Pages?

We can see from the screenshot below how a typical MVC project keeps the components of a single page in a set of files spread across many files and folders:

M V C

Picture: The MVC Project Structure

So, in MVC, there’s not a single “web page” file. And it’s a little awkward to explain this fact to someone who’s new to the technology.

What if you took an MVC application, then you called your View as a "Page" (e.g., in Index.cshtml file), and you centralized not only the Model data but also the server-side code related to that page (that used to reside on your Controller) inside a class dedicated to that page (inside an Index.cshtml.cs file) - that you now called a Page Model?

If you have already worked in native mobile apps, then you have probably seen something similar to this in the Model-View-ViewModel (MVVM) pattern.

Razorpages

Picture: Razor Page Components

Despite being different from MVC, Razor Pages still relies on ASP.NET Core MVC Framework. Once you create a new project with the Razor Pages template, Visual Studio configures the application via Startup.cs file to enable the ASP.NET Core MVC Framework, as we have just seen.

The template not only configures the new web application for MVC use, but also creates the Page folder and a set of Razor pages and page models for the example application:

Razor Pages Structure

Picture: Razor Page Files

Anatomy of a Razor Page

At first sight, a Razor Page looks pretty much like an ordinary ASP.NET MVC View file. But a Razor Page requires a new directive. Every Razor Page must start with the @page directive, which tells ASP.NET Core to treat it as a Razor page. The following image shows a little more detail about a typical razor page.

Anatomy Razor Page

Picture: Anatomy of a Razor Page
  • @page - Identifies the file as a Razor Page. Without it, the page is simply unreachable by ASP.NET Core
  • @model - much like in an MVC application, defines the class from which originates the binding data, as well as the Get/Post methods requested by the page
  • @using - the regular directive for defining namespaces
  • @inject - configures which interface(s) instance(s) should be injected into the page model class. @{ } - a piece of C# code inside Razor brackets, which in this case is used to define the page title

Creating a New User

Since we created a new SQLite database without users, our customers need to fill in the Identity's Register page. These are the links rendered by the _LoginPartial.cshtml partial view in the Layout.cshtml page:

Register Login And Icons

Now, let's create a new customer account named alice@smith.com.

Identity Account Register

Picture: The Register Page

When customers click on the Register link, they are redirected to the /Identity/Account/Register page. As we can see, ASP.NET Core Identity already provided a robust solution for a common user registration problem. Also, ASP.NET Core Identity pages are seamlessly integrated with our e-Commerce front end.

Identity Account Login

Picture: The Login Page

ASP.NET Core Identity also provides features that would require a lot of effort to implement, such as "Forgot your Password?" and user lockout (when users are blocked from login after entering wrong passwords multiple times).

Razor
@if (User.Identity.IsAuthenticated)
    {
        <ul class="nav navbar-nav">
            <li>
                <vc:notification-counter title="Notifications"...

                <vc:basket-counter title="Basket"...

            </li>
        </ul>
    }
Listing: Notification view components are shown only when the user is authenticated

Register Login No Icons

Click User Name

Mvc Db

Authorizing ASP.NET Core Resources

Basket Nonauthorized

Picture: Accessing the Basket Page Anonymously

Now that we have Identity working, we will begin to protect some areas of our MVC project from anonymous access, that is, unauthenticated access. This will ensure that only users who have entered a valid login and password can access protected system resources. But what resources should be protected against anonymous access?

Controller Should it be protected?
CatalogController No
BasketController Yes
CheckoutController Yes
NotificationsController Yes
RegistrationController Yes

Note that the CatalogController will be unprotected. Why? We want to allow users to browse the site's product freely, without forcing them to log in with the password. The other controllers are all protected, as they involve the handling of orders, which can only be done by customers. But how are we going to protect these resources? We must mark these controllers with an authorization attribute:

C#
[Authorize]
public class BasketController : BaseController
{
    public IActionResult Index()
    ...
C#
[Authorize]
public class BasketController : BaseController
...
C#
[Authorize]
public class CheckoutController : BaseController
...
C#
[Authorize]
public class NotificationsController : BaseController
...
C#
[Authorize]
public class RegistrationController : BaseController
...
Listing: Defining controller authorization

Let's take a test now: What happens when an anonymous user tries to access one of these features marked with [Authorize]? ASP.NET Core Identity will receive each of the requisitions made to the application. If the user is already authenticated, Identity passes the processing to the next component of the pipeline. If the user is anonymous and the resource being accessed requires an authorization, then Identity will redirect the user to the login page.

Running the application as an anonymous user, we go to the product search page, which we can access without any problem since this action is unprotected (that is without the [Authorize] attribute):

Add To Basket Nonauthorized

Picture: Adding Item to Basket Anonymously

When ASP.NET Core tries to execute the Index actin within the Basket controller, the [Authorize] attribute checks whether the user is authenticated. Since there is no authenticated user, ASP.NET Core redirects the request through the URL:

https://localhost:44340/Identity/Account/Login?ReturnUrl=%2FBasket

Returnurl

Picture: The Login Page

Note that this URL has two parts:

We can look at this redirection process more closely by opening the Developer Tools (Chrome Key F12) and opening the Headers tab, where we have seen that the call to the Action / Cart action is redirected via HTTP code 302, which is the code for redirection:

Redirect

Picture: The Login Redirection

So we closed the topic on ASP.NET Core Identity Configuration. From now on, we will begin to get the user information that can finally be used in our application.

Managing User Data

Preparing the User Registration Form

Once the user submits the form, the RegistrationViewModel must be ready to transport all the data.

Therefore, we will add custom user information to the registration view model class.

C#
public class RegistrationViewModel
{
    public string UserId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public string Address { get; set; }
    public string AdditionalAddress { get; set; }
    public string District { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}
Listing: Custom user information at /Models/ViewModels/RegistrationViewModel.cs
Razor
@using MVC.Models.ViewModels
@model RegistrationViewModel
@{
    ViewData["Title"] = "Registration";
}
<h3>Registration</h3>

<form method="post" asp-controller="checkout" asp-action="index">
    <input type="hidden" asp-for="@Model.UserId" />
    <div class="card">
        <div class="card-body">
            <div class="row">
                <div class="col-sm-4">
                    <div class="form-group">
                        <label class="control-label" for="name">Customer Name</label>
                        <input type="text" class="form-control" 
                         id="name" asp-for="@Model.Name" />
                        <span asp-validation-for="@Model.Name" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label class="control-label" for="email">Email</label>
                        <input type="email" class="form-control" id="email" 
                         asp-for="@Model.Email">
                        <span asp-validation-for="@Model.Email" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label class="control-label" for="phone">Phone</label>
                        <input type="text" class="form-control" 
                         id="phone" asp-for="@Model.Phone" />
                        <span asp-validation-for="@Model.Phone" class="text-danger"></span>
                    </div>
                </div>
                <div class="col-sm-4">
                    <div class="form-group">
                        <label class="control-label" for="address">Address</label>
                        <input type="text" class="form-control" id="address" 
                         asp-for="@Model.Address" />
                        <span asp-validation-for="@Model.Address" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label class="control-label" for="additionaladdress">
                               Additional Address</label>
                        <input type="text" class="form-control" 
                         id="additionaladdress" asp-for="@Model.AdditionalAddress" />
                        <span asp-validation-for="@Model.AdditionalAddress" 
                         class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label class="control-label" for="district">District</label>
                        <input type="text" class="form-control" id="district" 
                         asp-for="@Model.District" />
                        <span asp-validation-for="@Model.District" 
                         class="text-danger"></span>
                    </div>
                </div>
                <div class="col-sm-4">
                    <div class="form-group">
                        <label class="control-label" for="city">City</label>
                        <input type="text" class="form-control" id="city" 
                         asp-for="@Model.City" />
                        <span asp-validation-for="@Model.City" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label class="control-label" for="state">State</label>
                        <input type="text" class="form-control" 
                         id="state" asp-for="@Model.State" />
                        <span asp-validation-for="@Model.State" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        <label class="control-label" for="zipcode">Zip Code</label>
                        <input type="text" class="form-control" 
                         id="zipcode" asp-for="@Model.ZipCode" />
                        <span asp-validation-for="@Model.ZipCode" class="text-danger"></span>
                    </div>

                    <div class="form-group">
                        <a class="btn btn-success" href="/">
                            Keep buying
                        </a>
                    </div>
                    <div class="form-group">
                        <button type="submit"
                                class="btn btn-success button-notification">
                            Check out
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>
Listing: Customer registration view at /MVC/Views/Registration/Index.cshtml

Also, the new user information must be held by the AppIdentityUser class. And this is not just an ordinary class. It defines the model used to create or modify the database table for the User entity (table AspNetUsers).

C#
public class AppIdentityUser : IdentityUser
{
    public string Name { get; set; }
    public string Phone { get; set; }
    public string Address { get; set; }
    public string AdditionalAddress { get; set; }
    public string District { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}
Listing: The custom user information at /MVC/Areas/Identity/Data/AppIdentityUser.cs

But note that, once again, we must create a new Entity Framework Core Migration in order to apply these changes in the model to the database table.

To add a new migration, open the Tools > Package Manager Console menu, and type in the console.

PM> Add-Migration UserProfileData

The above command creates the "UserProfileData" migration containing the migration statements that adds the new user table fields:

Image 32

Picture: The Migration Project Folder

In order to create the SQLite database, you must apply the migration by executing the Update-Database command:

PM> Update-Database -verbose

This command compares the current model with the database snapshot and applies the differences back in the database. In our case, the differences detected are the missing user properties.

Retrieving User Data From Identity Database

Registration Form

Picture: The Registration Page

Filling in form fields is always a tedious task. But it some cases, it can't be avoided or postponed. Think about the customer information on an e-Commerce website: without all the correct data, the freight cannot be calculated and the shipping cannot be processed. But there are ways in which you can mitigate the customer's dissatisfaction with this procedure. You can save customer data for future orders, for example. But how can we save user data using ASP.NET Core Identity?

Fortunately, Identity comes in with a class named UserManager<T> (where T stands for the user class, or AppIdentityUser) that provides the APIs for managing users in a persistence store. That is, it exposes the functionalities needed to save and retrieve user data to and from the database.

The UserManager<T> parameter is passed via dependency injection. But don't worry configuring it in Startup class. Once you add Identity scaffolding, the IdentityHostingStartup class already registered the UserManager<T> type for dependency injection.

In the Index method of the RegistrationController, we can see how the GetUserAsync() method is used to asynchronously retrieve the current user from the Identity store (i.e., the SQLite database).

C#
[Authorize]
public class RegistrationController : BaseController
{
    private readonly UserManager userManager;

    public RegistrationController(UserManager<AppIdentityUser> userManager)
    {
        this.userManager = userManager;
    }

    public async Task<IActionResult> Index()
    {
        var user = await userManager.GetUserAsync(this.User);
        var viewModel = new RegistrationViewModel(
            user.Id, user.Name, user.Email, user.Phone,
            user.Address, user.AdditionalAddress, user.District,
            user.City, user.State, user.ZipCode
        );
        return View(viewModel);
    }
}
Listing: The /MVC/Controllers/RegistrationController.cs file

After that, we fill the RegistrationViewModel class with user data retrieved from AspNetUsers table. In turn, the view model is passed into the view to auto-fill the registration form for second-time customers, as we can see in the <input> fields below.

Razor
<input class="form-control" asp-for="@Model.Phone" />
...
<input class="form-control" asp-for="@Model.Address" />
...
<input class="form-control" asp-for="@Model.AdditionalAddress" />
...
<input class="form-control" asp-for="@Model.District" />
...
<input class="form-control" asp-for="@Model.City" />
...
<input class="form-control" asp-for="@Model.State" />
...
<input class="form-control" asp-for="@Model.ZipCode" />
...
Listing: New tag helpers at /MVC/Views/Registration/Index.cshtml

Persisting User Data to Identity Database

The code below shows how to:

  • Checks if the model is valid, that is, if RegistrationViewModel class validation rules are satisfied
  • Retrieves the user asynchronously by using the GetUserAsync() method
  • Modifies the user object coming from the database by applying the form data
  • Updates the SQLite database by using the UpdateAsync() method
  • Redirects back to Registration view if the model is invalid
C#
[Authorize]
public class CheckoutController : BaseController
{
    private readonly UserManager<AppIdentityUser> userManager;

    public CheckoutController(UserManager<AppIdentityUser> userManager)
    {
        this.userManager = userManager;
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Index(RegistrationViewModel registration)
    {
        if (ModelState.IsValid)
        {
            var user = await userManager.GetUserAsync(this.User);

            user.Email = registration.Email;
            user.Phone = registration.Phone;
            user.Name = registration.Name;
            user.Address = registration.Address;
            user.AdditionalAddress = registration.AdditionalAddress;
            user.District = registration.District;
            user.City = registration.City;
            user.State = registration.State;
            user.ZipCode = registration.ZipCode;

            await userManager.UpdateAsync(user);
            return View(registration);
        }
        return RedirectToAction("Index", "Registration");
    }
}
Listing: The Part 03/MVC/Controllers/CheckoutController.cs file

When the order is placed, the Checkout view shows the order confirmation with the "thank you" message from the website. Once again, we use the RegistrationViewModel as the source for the view binding.

Razor
@model RegistrationViewModel
...
<p>Thank you very much, <b>@Model.Name</b>!</p>
<p>Your order has been placed.</p>
<p>Soon you will receive an e-mail at <b>@Model.Email</b> including all order details.</p>
Listing: Binding checkout data at /MVC/Views/Checkout/Index.cshtml

Note how CheckoutController class checked if the model was valid before updating the database user information:

C#
public async Task<IActionResult> Index(RegistrationViewModel registration)
{ 
   if (ModelState.IsValid) 
   {

This verification is required on the server side so that we don't update the database table with inconsistent information. But this is only the last line of defense for our application. You should never depend uniquely on server-side checks for data validation. What else must be done? You should also impose an early validation, performing client-side checks at the moment the user tries to submit the order. Fortunately, the ASPNET Core project provides a partial view for client-side validation:

HTML
<environment include="Development">
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment exclude="Development">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.0/
                 jquery.validate.min.js"
            asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator"
            crossorigin="anonymous"
            integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/
                 jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
            asp-fallback-src="~/lib/jquery-validation-unobtrusive/
                               jquery.validate.unobtrusive.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.validator && 
                               window.jQuery.validator.unobtrusive"
            crossorigin="anonymous"
            integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
    </script>
</environment>
Listing: The _ValidationScriptsPartial.cshtml partial view

The validation partial view must be included in Registration view using the <partial> tag helper:

Razor
@section Scripts
{
    <partial name="~/Views/Shared/_ValidationScriptsPartial.cshtml"/>
}
Listing: Including form client validation scripts at \MVC\Views\Registration\Index.cshtml

Logging In With Microsoft Account, Google, Facebook, etc.

Why Allow External Account Login?

Registering a new user and password in a web application is often a tedious process, which in an e-commerce application can be not only disappointing to the client, but also detrimental to the business, as any additional steps may discourage potential buyers. They may prefer another e-commerce website that is friendlier and less bureaucratic. In short: forcing users to register can hurt sales.

An external login process, which allows us to integrate the identity process with existing accounts on external services such as Microsoft, Google and Facebook, and without the need to create new passwords, can provide a more convenient registration process for our clients.

However, this external login process must be implemented as an alternative, and not the only registration method.

Fortunately, ASP.NET Core Identity provides a mechanism to allow users to perform logon through external providers such as Microsoft, Google, Facebook, Twitter, etc.

Configuring External Logon with Microsoft Account

Keep in mind that external login services are not aware of your application, and vice versa. Both parties need a configuration that defines which applications / services will be involved in the authentication process.

Let's create the configuration needed so that our users can use their Microsoft accounts (@hotmail.com, @outlook.com, etc.) as a means of login for our application.

First, the Microsoft authentication service needs to know our application. We need to enter the address of the service called Microsoft Application Registration Portal https://apps.dev.microsoft.com and create the settings for our application.

First, you (the developer) need to log in with your Microsoft account to access the portal:

Enter Microsoft

Picture: Microsoft's Login Provider Page

In this developer portal, you can see your registered applications. If you still don't have a Microsoft account, you can create one. After the sign on, you will be redirected to the My Apps page:

Apps Dev Microsoft

Picture: My Apps at Microsoft Developer Portal

Here, you will register a new application. Select Add an Application in the upper right corner and enter the name of the application.

Let's give it a meaningful name, such as GroceryStore.

New Microsoft Appliction

Picture: Registering a New Application

Click Create Application to continue to the registration page. Provide a name and note the value of the application Id, which you can use as ClientId later.

Microsoft Application Properties

Picture: Generating a New App Password

Application Secrets

Picture: Defining App Platforms

Here, you will click on Add Platform in the platforms section and select the Web Platform.

Microsoft Add Platform

Picture: Available App Platforms

Under Web Platform, enter your development URL with /signin-microsoft added to the redirect field URLs (for example: https://localhost:44320/signin-microsoft). The Microsoft authentication scheme that we will configure later will automatically handle the requests in /signin-microsoft route to implement the OAuth flow:

Microsoft Redirect Url

Picture: Configuring App's Redirect URL

Note that on this page, we will click Add URL to ensure that the URL has been added.

Microsoft Redirect Url Add

Fill in any other application settings if necessary and click save at the bottom of the page to save changes to the application configuration.

Microsoft Application Save

Now, look at the Application Id that appears on the registration page. Click to generate a new password in the "application secrets" section. This will display a box in which you can copy the application password:

Microsoft App New Password

Picture: Obtaining the Password

Where will we store this password? In a real-world commercial application, we would have to use some form of secure storage, such as Environment Variables or the Secret Manager tool (https://docs.microsoft.com/aspnet/core/security/app-secrets?view=aspnetcore-2.2&tabs=windows).

However, to make our life easier, let's simply use the appsettings.json configuration file to store the application password registered on the Microsoft Developer Portal. Here, we created two new configuration keys:

  • ExternalLogin:Microsoft:ClientId: the web application ID created at Microsoft
  • ExternalLogin:Microsoft:ClientSecret: the password of the web application created in Microsoft
JavaScript
"ExternalLogin": {
  "Microsoft": {
    "ClientId": "nononononononononononnnononoon",
    "ClientSecret": "nononononononononononnnononoon"
  }
}
Listing: External login configuration at appsettings.json

Now let's add the following excerpt to the CofigureServices method of the Startup class to enable authentication through Microsoft account:

JavaScript
services.AddAuthentication()
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = Configuration["ExternalLogin:Microsoft:ClientId"];
        options.ClientSecret = Configuration["ExternalLogin:Microsoft:ClientSecret"];
    });
Listing: External login configuration at Startup class

Running our e-commerce application again, we can see on the login page a right panel, where you can find a new button that allows you to sign in with the Microsoft external provider.

Microsoft

Picture: The New Microsoft External Login Option

After logging in to the Microsoft page, our user is redirected to an "account association page" provided by ASP.NET Core Identity. Here, we will click on "Register" to complete the association between the Microsoft account and our e-commerce account:

Associate Your Microsoft Account

Picture: Associating Accounts

As we can see below, the client is now registered with Microsoft's email, and no further login information is required by our application!

Loggedin As Microsoft

Picture: Logged in With a Microsoft Account

Note that accounts created directly by Identity can coexist side by side with user accounts created in external mechanisms such as Google, Microsoft, Facebook, etc. as we can see in the user table (AspNetUsers) of the SQLite database MVC.db: Sqlite Aspnetuser Microsoft

Picture: The AspNetUsers SQLite Table

Eventually, we can investigate which user accounts were created outside our system. Just open the AspNetUserLogins table from the MVC.db database:

Aspnetuserlogins

Picture: the AspNetUserLogins SQLite Table

Configuring External Logon with Google Account

Now let's create the configuration needed so that our users can use Google accounts (@gmail.com) as an alternative means of login for our application.

Google authentication service also needs to know our application. We need to go to Google Sign-In for Websites first. At that page, you must click to configure your project:

Image 49

Picture: Integrating Google Sign-In into your web app

Now you must configure a project for Google Sign-in. Enter the name for your project here. Let's give it a meaningful name, such as GroceryStore:

Image 50

Picture: Configure a project for Google Sign-in

Now it's time to configure your OAuth client. Type in a friendly name of your app to be presented to users when they sign in with their Google accounts:

Image 51

Picture: Configure your OAuth client

Next, you tell Google where your app is calling from. In this case, we go with Web server, because it's our ASP.NET Core Web application that will call Google external login provider for user authentication.

Image 52

Picture: Where are you calling from?

Google also needs our app's redirect URI. As soon as our users are authenticated with Google, the http://localhost:5001/signin-google URI will be called with the authorization code for access.

Image 53

Picture: Authorized redirect URI

After clicking the Create button, you can check the newly created Client ID and Client Secret values that need to be used in your application.

Now, let's open the appsettings.json file and insert the following keys and values:

  • ExternalLogin:Google:ClientId: the web application ID created at Google
  • ExternalLogin:Google:ClientSecret: the password of the web application created in Google
JavaScript
"ExternalLogin": {
  "Microsoft": {
    "ClientId": "nononononononononononnnononoon",
    "ClientSecret": "nononononononononononnnononoon"
  },
  "Google": {
    "ClientId": "nononononononononononnnononoon",
    "ClientSecret": "nononononononononononnnononoon"
  }
}
Listing: External login configuration at appsettings.json

Now let's add the following excerpt to the CofigureServices method of the Startup class to enable authentication through Google account:

JavaScript
services.AddAuthentication()
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = Configuration["ExternalLogin:Microsoft:ClientId"];
        options.ClientSecret = Configuration["ExternalLogin:Microsoft:ClientSecret"];
    })
    .AddGoogle(options =>
    {
        options.ClientId = Configuration["ExternalLogin:Google:ClientId"];
        options.ClientSecret = Configuration["ExternalLogin:Google:ClientSecret"];
    });
Listing: External login configuration at Startup class

Running our e-commerce application again, we can see on the login page a right panel, where you can find a new button that allows you to sign in with the Google external provider.

Microsoft Google

Picture: The New Google External Login Option

After logging in to the Microsoft page, our user is redirected to an "account association page" provided by ASP.NET Core Identity. Here, we will click on "Register" to complete the association between the Google account and our e-commerce account:

Associate Your Microsoft Account

Picture: Associating Accounts

As we can see below, the client is now registered with Google's email, and no further login information is required by our application!

Enter Google

Picture: Choosing a Google Account

Loggedin As Google

Configuring External Login with Google Account

Finally, let's add the call to the AddGoogle() extension method to the CofigureServices method of the Startup class to enable authentication via Google account:

JavaScript
services.AddAuthentication()
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = Configuration["ExternalLogin:Microsoft:ClientId"];
        options.ClientSecret = Configuration["ExternalLogin:Microsoft:ClientSecret"];
    })
    .AddGoogle(options =>
    {
        options.ClientId = Configuration["ExternalLogin:Google:ClientId"];
        options.ClientSecret = Configuration["ExternalLogin:Google:ClientSecret"];
    });
Listing: Adding external Google login provider at Startup class

Conclusion

We reached the end of "ASP.NET Core Road to Microservices Part 3". In this article, we understand the user authentication needs of the application we had at the end of the last course (ASP.NET Core Road to Microservices Part 2). We then decided to use ASP.NET Core Identity as an authentication solution for our e-commerce web application.

We have learned how to use the ASP.NET Core Identity identity creation process to authorize the MVC application so that it can take advantage of the benefits of user authentication, resource protection, and sensitive pages such as Basket, Registration, and Checkout.

We understand how the user layout flow works and we have learned how to configure the MVC Web application to meet the requirements of that stream. As soon as the login and logout process is understood, we begin to modify our MVC application to use both the id and the user name, as well as the other registration information of each logged in user, which in the context of our MVC application represents the client that is making the purchase.

Finally, we learned how to execute an external login process, which allows us to integrate the identity process with existing accounts in external services such as Microsoft, Google and Facebook, thus providing a more convenient registration process for our customers.

History

  • 29th June, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Instructor / Trainer Alura Cursos Online
Brazil Brazil

Comments and Discussions

 
QuestionAfter 3, articles are not visible. Pin
Rajeev Singh Chib25-Jun-20 4:39
Rajeev Singh Chib25-Jun-20 4:39 
SuggestionSome Images are missing Pin
Dush Abe8-Apr-20 23:56
Dush Abe8-Apr-20 23:56 
Most of the code or structure image links are broken, if you can fix them the article will be more useful to the learners.
QuestionWhat to do with an anonymous user ؟ Pin
hasanbaba202030-Nov-19 9:03
hasanbaba202030-Nov-19 9:03 
QuestionFYI: Download not working in Chrome but OK in Firefox Pin
Howard Payne31-Oct-19 3:24
Howard Payne31-Oct-19 3:24 
GeneralImages are broken Pin
tyaramis24-Oct-19 23:45
tyaramis24-Oct-19 23:45 
QuestionImages Pin
Богдан Акопян11-Jul-19 10:05
Богдан Акопян11-Jul-19 10:05 
QuestionMore series this year ? Pin
kiquenet.com6-Jul-19 21:19
professionalkiquenet.com6-Jul-19 21:19 
AnswerRe: More series this year ? Pin
Marcelo Ricardo de Oliveira9-Jul-19 5:07
mvaMarcelo Ricardo de Oliveira9-Jul-19 5:07 
GeneralRe: More series this year ? Pin
cplas4-Nov-19 11:58
cplas4-Nov-19 11:58 
QuestionWell written - BUT Pin
Member 95291874-Jul-19 23:43
Member 95291874-Jul-19 23:43 
QuestionMissing File Pin
Jeff Bowman4-Jul-19 15:13
professionalJeff Bowman4-Jul-19 15:13 
QuestionNot found Pin
CsLacy1-Jul-19 0:02
professionalCsLacy1-Jul-19 0:02 
AnswerRe: Not found Pin
Marcelo Ricardo de Oliveira1-Jul-19 4:29
mvaMarcelo Ricardo de Oliveira1-Jul-19 4:29 
GeneralRe: Not found Pin
CsLacy1-Jul-19 4:51
professionalCsLacy1-Jul-19 4:51 
GeneralRe: Not found Pin
Marcelo Ricardo de Oliveira1-Jul-19 7:40
mvaMarcelo Ricardo de Oliveira1-Jul-19 7:40 
GeneralRe: Not found Pin
Member 137479361-Jul-19 10:51
Member 137479361-Jul-19 10:51 
GeneralRe: Not found Pin
Marcelo Ricardo de Oliveira1-Jul-19 10:59
mvaMarcelo Ricardo de Oliveira1-Jul-19 10:59 
GeneralRe: Not found Pin
Member 137479361-Jul-19 11:15
Member 137479361-Jul-19 11:15 
GeneralRe: Not found Pin
Marcelo Ricardo de Oliveira1-Jul-19 11:51
mvaMarcelo Ricardo de Oliveira1-Jul-19 11:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.