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
.
Picture: The project context menu
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
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.
Picture: The Project Context Menu
Here, we will choose Installed > Identity > Identity.
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.
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.
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:
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:
.
.
.
"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:
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:
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:
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
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.
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.
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:
...
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
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.
Picture: The _LoginPartial.cshtml Partial View
@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:
<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:
<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:
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:
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:
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.
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:
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.
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:
Now, let's create a new customer account named alice@smith.com.
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.
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).
@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
Authorizing ASP.NET Core Resources
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:
[Authorize]
public class BasketController : BaseController
{
public IActionResult Index()
...
[Authorize]
public class BasketController : BaseController
...
[Authorize]
public class CheckoutController : BaseController
...
[Authorize]
public class NotificationsController : BaseController
...
[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):
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
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:
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
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.
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
@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
).
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:
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
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).
[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.
<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
[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.
@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:
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:
<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:
@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:
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:
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
.
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.
Picture: Generating a New App Password
Picture: Defining App Platforms
Here, you will click on Add Platform in the platforms section and select the Web 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:
Picture: Configuring App's Redirect URL
Note that on this page, we will click Add URL to ensure that the URL has been added.
Fill in any other application settings if necessary and click save at the bottom of the page to save changes to the application configuration.
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:
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
"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:
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.
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:
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!
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:
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:
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:
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
:
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:
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.
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.
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
"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:
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.
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:
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!
Picture: Choosing a Google Account
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:
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