Part 4 Securing and deploying your applications
So far in the book you’ve learned how to use minimal APIs,Razor Pages, and Model-View-Controller (MVC) controllers to build both server-rendered applications and APIs. You know how to dynamically generate JavaScript Object Notation (JSON) and HTML code based on incoming requests, and how to use configuration and dependency injection to customize your app’s behavior at runtime. In part 4 you’ll learn how to add users and profiles to your app and how to publish and secure your apps.
In chapters 23 through 25 you’ll learn how to protect your applications with authentication and authorization. In chapter 23 you’ll see how you can add ASP.NET Core Identity to your apps so that users can log in and enjoy a customized experience. You’ll learn how to protect your Razor Pages apps using authorization in chapter 24 so that only some users can access certain pages in your app. In chapter 25 you’ll learn how to apply the same protections to your minimal API and web API applications.
Adding logging to your application is one of those activities that’s often left until after you discover a problem in production. Adding sensible logging from the get-go will help you quickly diagnose and fix errors as they arise. Chapter 26 introduces the logging framework built into ASP.NET Core. You’ll see how you can use it to write log messages to a wide variety of locations, whether it’s the console, a file, or a third-party remote-logging service.
By this point you’ll have all the fundamentals to build a production application with ASP.NET Core. In chapter 27 I cover the steps required to make your app live, including how to publish an app to Internet Information Services (IIS) and how to configure the URLs your app listens on.
Before you expose your application to the world, an important part of web development is securing your app correctly. Even if you don’t feel you have any sensitive data in your application, you must make sure to protect your users from attacks by adhering to security best practices. You’ll learn how to configure HTTPS for your application in chapter 28 and why this is a vital step for modern web development. Similarly, in chapter 29 I describe some common security vulnerabilities, how attackers can exploit them, and what you can do to protect your applications.
23 Authentication: Adding users to your application with Identity
This chapter covers
• Seeing how authentication works in web apps in ASP.NET Core
• Creating a project using the ASP.NET Core Identity system
• Adding user functionality to an existing web app
• Customizing the default ASP.NET Core Identity UI
One of the selling points of a web framework like ASP.NET Core is the ability to provide a dynamic app, customized to individual users. Many apps have the concept of an “account” with the service, which you can “sign in” to and get a different experience.
Depending on the service, an account gives you varying things. On some apps you may have to sign in to get access to additional features, and on others you might see suggested articles. On an e-commerce app, you’d be able to place orders and view your past orders; on Stack Overflow you can post questions and answers; on a news site you might get a customized experience based on previous articles you’ve viewed.
When you think about adding users to your application, you typically have two aspects to consider:
• Authentication—The process of creating users and letting them log in to your app
• Authorization—Customizing the experience and controlling what users can do, based on the current logged-in user
In this chapter I’m going to be discussing the first of these points, authentication and membership. In the next chapter I’ll tackle the second point, authorization. In section 23.1 I discuss the difference between authentication and authorization, how authentication works in a traditional ASP.NET Core web app, and ways you can architect your system to provide sign-in functionality. I don’t discuss API applications in detail in this chapter, though many of the authentication principles apply to both styles of app. I discuss API applications chapter 25.
In section 23.2 I introduce a user-management system called ASP.NET Core Identity (Identity for short). Identity integrates with Entity Framework Core (EF Core) and provides services for creating and managing users, storing and validating passwords, and signing users in and out of your app.
In section 23.3 you’ll create an app using a default template that includes ASP.NET Core Identity out of the box. This gives you an app to explore and see the features Identity provides, as well as everything it doesn’t.
Creating an app is great for seeing how the pieces fit together, but you’ll often need to add users and authentication to an existing app. In section 23.4 you’ll see the steps required to add ASP.NET Core Identity to an existing app.
In sections 23.5 and 23.6 you’ll learn how to replace pages from the default Identity UI by scaffolding individual pages. In section 23.5 you’ll see how to customize the Razor templates to generate different HTML on the user registration page, and in section 23.6 you’ll learn how to customize the logic associated with a Razor Page. You’ll see how to store additional information about a user (such as their name or date of birth) and how to provide them permissions that you can later use to customize the app’s behavior (if the user is a VIP, for example).
Before we look at the ASP.NET Core Identity system specifically, let’s take a look at authentication and authorization in ASP.NET Core—what’s happening when you sign in to a website and how you can design your apps to provide this functionality.
23.1 Introducing authentication and authorization
When you add sign-in functionality to your app and control access to certain functions based on the currently signed-in user, you’re using two distinct aspects of security:
• Authentication—The process of determining who you are
• Authorization—The process of determining what you’re allowed to do
Generally you need to know who the user is before you can determine what they’re allowed to do, so authentication always comes first, followed by authorization. In this chapter we’re looking only at authentication; we’ll cover authorization in chapter 24.
In this section I start by discussing how ASP.NET Core thinks about users, and I cover some of the terminology and concepts that are central to authentication. I found this to be the hardest part to grasp when I learned about authentication, so I’ll take it slow.
Next, we’ll look at what it means to sign in to a traditional web app. After all, you only provide your password and sign into an app on a single page; how does the app know the request came from you for subsequent requests?
23.1.1 Understanding users and claims in ASP.NET Core
The concept of a user is baked into ASP.NET Core. In chapter 3 you learned that the HTTP server, Kestrel, creates an HttpContext object for every request it receives. This object is responsible for storing all the details related to that request, such as the request URL, any headers sent, and the body of the request.
The HttpContext object also exposes the current principal for a request as the User property. This is ASP.NET Core’s view of which user made the request. Any time your app needs to know who the current user is or what they’re allowed to do, it can look at the HttpContext.User principal.
DEFINITION You can think of the principal as the user of your app.
In ASP.NET Core, principals are implemented using the ClaimsPrincipal class, which has a collection of claims associated with it, as shown in figure 23.1.
Figure 23.1 The principal is the current user, implemented as ClaimsPrincipal. It contains a collection of Claims that describe the user.
You can think about claims as properties of the current user. For example, you could have claims for things like email, name, and date of birth.
DEFINITION A claim is a single piece of information about a principal; it consists of a claim type and an optional value.
Claims can also be indirectly related to permissions and authorization, so you could have a claim called HasAdminAccess or IsVipCustomer. These would be stored in the same way—as claims associated with the user principal.
NOTE Earlier versions of ASP.NET used a role-based approach to security rather than a claims-based approach. The ClaimsPrincipal used in ASP.NET Core is compatible with this approach for legacy reasons, but you should use the claims-based approach for new apps.
Kestrel assigns a user principal to every request that arrives at your app. Initially, that principal is a generic, anonymous, unauthenticated principal with no claims. How do you log in, and how does ASP.NET Core know that you’ve logged in on subsequent requests?
In the next section we’ll look at how authentication works in a traditional web app using ASP.NET Core and the process of signing into a user account.
23.1.2 Authentication in ASP.NET Core: Services and middleware
Adding authentication to any web app involves a few moving parts. The same general process applies whether you’re building a traditional web app or a client-side app (though there are often differences in the latter, as I discuss in chapter 25):
-
The client sends an identifier and a secret to the app to identify the current user. For example, you could send an email address (identifier) and a password (secret).
-
The app verifies that the identifier corresponds to a user known by the app and that the corresponding secret is correct.
-
If the identifier and secret are valid, the app can set the principal for the current request, but it also needs a way of storing these details for subsequent requests. For traditional web apps, this is typically achieved by storing an encrypted version of the user principal in a cookie.
This is the typical flow for most web apps, but in this section I’m going to look at how it works in ASP.NET Core. The overall process is the same, but it’s good to see how this pattern fits into the services, middleware, and Model-View-Controller (MVC) aspects of an ASP.NET Core application. We’ll step through the various pieces at play in a typical app when you sign in as a user, what that means, and how you can make subsequent requests as that user.
Signing in to an ASP.NET Core application
When you first arrive on a site and sign in to a traditional web app, the app will send you to a sign-in page and ask you to enter your username and password. After you submit the form to the server, the app redirects you to a new page, and you’re magically logged in! Figure 23.2 shows what’s happening behind the scenes in an ASP.NET Core app when you submit the form.
Figure 23.2 Signing in to an ASP.NET Core application. SignInManager is responsible for setting HttpContext.User to the new principal and serializing the principal to the encrypted cookie.
This figure shows the series of steps from the moment you submit the login form on a Razor Page to the point the redirect is returned to the browser. When the request first arrives, Kestrel creates an anonymous user principal and assigns it to the HttpContext.User property. The request is then routed to the Login.cshtml Razor Page, which reads the email and password from the request using model binding.
The meaty work happens inside the SignInManager service. This is responsible for loading a user entity with the provided username from the database and validating that the password they provided is correct.
Warning Never store passwords in the database directly. They should be hashed using a strong one-way algorithm. The ASP.NET Core Identity system does this for you, but it’s always wise to reiterate this point!
If the password is correct, SignInManager creates a new ClaimsPrincipal from the user entity it loaded from the database and adds the appropriate claims, such as the email address. It then replaces the old, anonymous HttpContext.User principal with the new, authenticated principal.
Finally, SignInManager serializes the principal, encrypts it, and stores it as a cookie. A cookie is a small piece of text that’s sent back and forth between the browser and your app along with each request, consisting of a name and a value.
This authentication process explains how you can set the user for a request when they first log in to your app, but what about subsequent requests? You send your password only when you first log in to an app, so how does the app know that it’s the same user making the request?
Authenticating users for subsequent requests
The key to persisting your identity across multiple requests lies in the final step of figure 23.2, where you serialized the principal in a cookie. Browsers automatically send this cookie with all requests made to your app, so you don’t need to provide your password with every request.
ASP.NET Core uses the authentication cookie sent with the requests to rehydrate a ClaimsPrincipal and set the HttpContext.User principal for the request, as shown in figure 23.3. The important thing to note is when this process happens—in the AuthenticationMiddleware.
Figure 23.3 A subsequent request after signing in to an application. The cookie sent with the request contains the user principal, which is validated and used to authenticate the request.
When a request containing the authentication cookie is received, Kestrel creates the default, unauthenticated, anonymous principal and assigns it to the HttpContext.User principal. Any middleware that runs before the AuthenticationMiddleware sees the request as unauthenticated, even if there’s a valid cookie.
Tip If it looks like your authentication system isn’t working, double-check your middleware pipeline. Only middleware that runs after AuthenticationMiddleware will see the request as authenticated.
The AuthenticationMiddleware is responsible for setting the current user for a request. The middleware calls the authentication services, which reads the cookie from the request, decrypts it, and deserializes it to obtain the ClaimsPrincipal created when the user logged in.
The AuthenticationMiddleware sets the HttpContext.User principal to the new, authenticated principal. All subsequent middleware now knows the user principal for the request and can adjust its behavior accordingly (for example, displaying the user’s name on the home page or restricting access to some areas of the app).
NOTE The AuthenticationMiddleware is responsible only for authenticating incoming requests and setting the ClaimsPrincipal if the request contains an authentication cookie. It is not responsible for redirecting unauthenticated requests to the login page or rejecting unauthorized requests; that is handled by the AuthorizationMiddleware, as you’ll see in chapter 24.
The process described so far, in which a single app authenticates the user when they log in and sets a cookie that’s read on subsequent requests, is common with traditional web apps, but it isn’t the only possibility. In chapter 25 we’ll take a look at authentication for web API applications, used by client-side and mobile apps and at how the authentication system changes for those scenarios.
Another thing to consider is where you store the authentication details for users of your app. In figure 23.2 I showed the authentication services loading the user authentication details from your app’s database, but that’s only one option.
Another option is to delegate the authentication responsibilities to a third-party identity provider, such as Okta, Auth0, Azure Active Directory B2B/B2C, or even Facebook. These manage users for you, so user information and passwords are stored in their database rather than your own. The biggest advantage of this approach is that you don’t have to worry about making sure your customer data is safe; you can be pretty sure that a third party will protect it, as it’s their whole business.
Tip Wherever possible, I recommend this approach, as it delegates security responsibilities to someone else. You can’t lose your users’ details if you never had them! Make sure to understand the differences in providers, however. With a provider like Auth0, you would own the profiles created, whereas with a provider like Facebook, you don’t!
Each provider provides instructions on how to integrate with their identity services, ideally using the OpenID Connect (OIDC) specification. This typically involves configuring some authentication services in your application, adding some configuration, and delegating the authentication process itself to the external provider. These providers can be used with your API apps too, as I discuss in chapter 25.
NOTE Hooking up your apps and APIs to use an identity provider can require a fair amount of tedious configuration, both in the app and the identity provider, but if you follow the provider’s documentation you should have plain sailing. For example, you can follow the documentation for adding authentication to a traditional web app using Microsoft’s Identity Platform here: http://mng.bz/4D9w.
While I recommend using an external identity provider where possible, sometimes you really want to store all the authentication details of your users directly in your app. That’s the approach I describe in this chapter.
ASP.NET Core Identity (hereafter shortened to Identity) is a system that makes building the user-management aspect of your app. It handles all the boilerplate for saving and loading users to a database, as well as best practices for security, such as user lockout, password hashing, and multifactor authentication.
DEFINITION Multifactor authentication (MFA), and the subset two-factor authentication (2FA) require both a password and an extra piece of information to sign in. This could involve sending a code to a user’s phone by Short Message Service (SMS) or using a mobile app to generate a code, for example.
In the next section I’m going to talk about the ASP.NET Core Identity system, the problems it solves, when you’d want to use it, and when you might not want to use it. In section 23.3 we take a look at some code and see ASP.NET Core Identity in action.
23.2 What is ASP.NET Core Identity?
Whenever you need to add nontrivial behaviors to your application, you typically need to add users and authentication. That means you’ll need a way of persisting details about your users, such as their usernames and passwords.
This might seem like a relatively simple requirement, but given that this is related to security and people’s personal details, it’s important you get it right. As well as storing the claims for each user, it’s important to store passwords using a strong hashing algorithm to allow users to use MFA where possible and to protect against brute-force attacks, to name a few of the many requirements. Although it’s perfectly possible to write all the code to do this manually and to build your own authentication and membership system, I highly recommend you don’t.
I’ve already mentioned third-party identity providers such as Auth0 and Azure Active Directory. These Software as a Service (SaaS) solutions take care of the user-management and authentication aspects of your app for you. If you’re in the process of moving apps to the cloud generally, solutions like these can make a lot of sense.
If you can’t or don’t want to use these third-party solutions, I recommend you consider using the ASP.NET Core Identity system to store and manage user details in your database. ASP.NET Core Identity takes care of most of the boilerplate associated with authentication, but it remains flexible and lets you control the login process for users if you need to.
NOTE ASP.NET Core Identity is an evolution of the legacy .NET Framework ASP.NET Identity system, with some design improvements and update to work with ASP.NET Core.
By default, ASP.NET Core Identity uses EF Core to store user details in the database. If you’re already using EF Core in your project, this is a perfect fit. Alternatively, it’s possible to write your own stores for loading and saving user details in another way.
Identity takes care of the low-level parts of user management, as shown in table 23.1. As you can see from this list, Identity gives you a lot, but not everything—by a long shot!
Table 23.1 Which services are and aren’t handled by ASP.NET Core Identity
Managed by ASP.NET Core Identity | Requires implementing by the developer |
---|---|
Database schema for storing users and claims | UI for logging in, creating, and managing users (Razor Pages or controllers); included in an optional package that provides a default UI |
Creating a user in the database | Sending email messages |
Password validation and rules | Customizing claims for users (adding new claims) |
Handling user account lockout (to prevent brute-force attacks) | Configuring third-party identity providers |
Managing and generating MFA/2FA codes | Integration into MFA such as sending SMS messages, time-based one-time password (TOTP) authenticator apps, or hardware keys |
Generating password-reset tokens | - |
Saving additional claims to the database | - |
Managing third-party identity providers (for example, Facebook, Google, and Twitter) | - |
The biggest missing piece is the fact that you need to provide all the UI for the application, as well as tying all the individual Identity services together to create a functioning sign-in process. That’s a big missing piece, but it makes the Identity system extremely flexible.
Luckily, ASP.NET Core includes a helper NuGet library, Microsoft.AspNetCore.Identity.UI, that gives you the whole of the UI boilerplate for free. That’s over 30 Razor Pages with functionality for logging in, registering users, using 2FA, and using external login providers, among other features. You can still customize these pages if you need to, but having a whole login process working out of the box, with no code required on your part, is a huge win. We’ll look at this library and how you use it in sections 23.3 and 23.4.
For that reason, I strongly recommend using the default UI as a starting point, whether you’re creating an app or adding user management to an existing app. But the question remains as to when you should use Identity and when you should consider rolling your own.
I’m a big fan of Identity when you need to store your own users, so I tend to suggest it in most situations, as it handles a lot of security-related things for you that are easy to mess up. I’ve heard several arguments against it, some valid and others less so:
• I already have user authentication in my app. Great! In that case, you’re probably right, Identity may not be necessary. But does your custom implementation use MFA? Do you have account lockout? If not, and if you need to add them, considering Identity may be worthwhile.
• I don’t want to use EF Core. That’s a reasonable stance. You could be using Dapper, some other object-relational mapper (ORM), or even a document database for your database access. Luckily, the database integration in Identity is pluggable, so you could swap out the EF Core integration and use your own database integration libraries instead.
• My use case is too complex for Identity. Identity provides lower-level services for authentication, so you can compose the pieces however you like. It’s also extensible, so if you need to, for example, transform claims before creating a principal, you can.
• I don’t like the default Razor Pages UI. The default UI for Identity is entirely optional. You can still use the Identity services and user management but provide your own UI for logging in and registering users. However, be aware that although doing this gives you a lot of flexibility, it’s also easy to introduce a security flaw in your user-management system—the last place you want security flaws!
• I’m not using Bootstrap to style my application. The default Identity UI uses Bootstrap as a styling framework, the same as the default ASP.NET Core templates. Unfortunately, you can’t easily change that, so if you’re using a different framework or need to customize the HTML generated, you can still use Identity, but you’ll need to provide your own UI.
• I don’t want to build my own identity system. I’m glad to hear it. Using an external identity provider like Azure Active Directory or Auth0 is a great way of shifting the responsibility and risk associated with storing users’ personal information to a third party.
Any time you’re considering adding user management to your ASP.NET Core application, I’d recommend looking at Identity as a great option for doing so. In the next section I’ll demonstrate what Identity provides by creating a new Razor Pages application using the default Identity UI. In section 23.4 we’ll take that template and apply it to an existing app instead, and in sections 23.5 and 23.6 you’ll see how to override the default pages.
23.3 Creating a project that uses ASP.NET Core Identity
I’ve covered authentication and Identity in general terms, but the best way to get a feel for it is to see some working code. In this section we’re going to look at the default code generated by the ASP.NET Core templates with Identity, how the project works, and where Identity fits in.
23.3.1 Creating the project from a template
You’ll start by using the Visual Studio templates to generate a simple Razor Pages application that uses Identity for storing individual user accounts in a database.
Tip You can create a similar project using the .NET CLI by running dotnet new webapp -au Individual. The Visual Studio template uses a LocalDB database, but the dotnet new template uses SQLite by default. To use LocalDB instead, run dotnet new webapp -au Individual --use-local-db.
To create the template using Visual Studio, you must be using the 2022 version or later and have the .NET 7 software development kit (SDK) installed. Follow these steps:
- Choose
File > New > Project
or choose Create a New Project on the splash screen. - From the list of templates, choose ASP.NET Core Web Application, ensuring that you select the C# language template.
- On the next screen, enter a project name, location, and a solution name, and choose Create.
- On the Additional Information screen, change the Authentication type to Individual Accounts, as shown in figure 23.4. Leave the other settings at their defaults, and choose Create to create the application.
Visual Studio automatically runs dotnet restore to restore all the necessary NuGet packages for the project.
Figure 23.4 Choosing the authentication mode of the new ASP.NET Core application template in VS 2022
- Run the application to see the default app, as shown in figure 23.5.
NOTE The Visual Studio template configures the application to use LocalDB and includes EF Core migrations for SQL Server. If you want to use a different database provider, you can replace the configuration and migrations with your database of choice, as described in chapter 12.
Figure 23.5 The default template with individual account authentication looks similar to the no authentication template, with the addition of a Login widget at the top right of the page.
This template should look familiar, with one twist: you now have Register and Login buttons! Feel free to play with the template—creating a user, logging in and out—to get a feel for the app. Once you’re happy, look at the code generated by the template and the boilerplate it saved you from writing.
Tip Don’t forget to run the included EF Core migrations before trying to create users. Run dotnet ef database update from the project folder.
23.3.2 Exploring the template in Solution Explorer
The project generated by the template, shown in figure 23.6, is similar to the default no-authentication template. That’s largely due to the default UI library, which brings in a big chunk of functionality without exposing you to the nitty-gritty details.
Figure 23.6 The project layout of the default template with individual authentication
The biggest addition is the Areas folder in the root of your project, which contains an Identity subfolder. Areas are sometimes used for organizing sections of functionality. Each area can contain its own Pages folder, which is analogous to the main Pages folder in your application.
DEFINITION Areas are used to group Razor Pages into separate hierarchies for organizational purposes. I rarely use areas and prefer to create subfolders in the main Pages folder instead. The one exception is the Identity UI, which uses a separate Identity area by default. For more details on areas, see Microsoft’s “Areas in ASP.NET Core” documentation: http://mng.bz/7Vw9.
The Microsoft.AspNetCore.Identity.UI package creates Razor Pages in the Identity area. You can override any page in this default UI by creating a corresponding page in the Areas/Identity/Pages folder in your application. In figure 23.6, the default template adds a _ViewStart.cshtml file that overrides the template that is included as part of the default UI. This file contains the following code, which sets the default Identity UI Razor Pages to use your project’s default _Layout.cshtml file:
@{
Layout = "/Pages/Shared/_Layout.cshtml";
}
Some obvious questions at this point are “How do you know what’s included in the default UI?” and “Which files can you override?” You’ll see the answers to both in section 23.5, but in general you should try to avoid overriding files where possible. After all, the goal with the default UI is to reduce the amount of code you have to write!
The Data folder in your new project template contains your application’s EF Core DbContext, called ApplicationDbContext, and the migrations for configuring the database schema to use Identity. I’ll discuss this schema in more detail in section 23.3.3.
The final additional file included in this template compared with the no-authentication version is the partial Razor view Pages/Shared/_LoginPartial.cshtml. This provides the Register and Login links you saw in figure 23.5, and it’s rendered in the default Razor layout, _Layout.cshtml.
If you look inside _LoginPartial.cshtml, you can see how routing works with areas by combining the Razor Page path with an {area} route parameter using Tag Helpers. For example, the Login link specifies that the Razor Page /Account/Login is in the Identity area using the asp-area attribute:
<a asp-area="Identity" asp-page="/Account/Login">Login</a>
Tip You can reference Razor Pages in the Identity area by setting the area route value to Identity. You can use the asp-area attribute in Tag Helpers that generate links.
In addition to viewing the new files included thanks to ASP.NET Core Identity, open Program.cs and look at the changes there. The most obvious change is the additional configuration, which adds all the services Identity requires, as shown in the following listing.
Listing 23.1 Adding ASP.NET Core Identity services to ConfigureServices
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
string connectionString = builder.Configuration #A
.GetConnectionString("DefaultConnection"); #A
builder.Services.AddDbContext<ApplicationDbContext>(options => #A
options.UseSqlServer(connectionString)); #A
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); #B
builder.Services.AddDefaultIdentity<IdentityUser>(options => #C
options.SignIn.RequireConfirmedAccount = true) #D
.AddEntityFrameworkStores<ApplicationDbContext>(); #E
builder.Services.AddRazorPages();
// remaining configuration not show
❶ ASP.NET Core Identity uses EF Core, so it includes the standard EF Core configuration.
❷ Adds optional database services to enhance the DeveloperExceptionPage
❸ Adds the Identity system, including the default UI, and configures the user type as IdentityUser
❹ Requires users to confirm their accounts (typically by email) before they log in
❺ Configures Identity to store its data in EF Core
The AddDefaultIdentity() extension method does several things:
• Adds the core ASP.NET Core Identity services.
• Configures the application user type to be IdentityUser. This is the entity model that is stored in the database and represents a “user” in your application. You can extend this type if you need to, but that’s not always necessary, as you’ll see in section 23.6.
• Adds the default UI Razor Pages for registering, logging in, and managing users.
• Configures token providers for generating MFA and email confirmation tokens.
Where is the authentication middleware?
If you’re already familiar with previous versions of ASP.NET Core, you might be surprised to notice the lack of any authentication middleware in the default template. Given everything you’ve learned about how authentication works, that should be surprising!The answer to this riddle is that the authentication middleware is in the pipeline, even though you can’t see it. As I discussed in chapter 4, WebApplication automatically adds many middleware components to the pipeline for you, including the routing middleware, the endpoint middleware, and—yes—the authentication middleware. So the reason you don’t see it in the pipeline is that it’s already been added.
In fact, WebApplication also automatically adds the authorization middleware to the pipeline, but in this case the template still calls UseAuthorization(). Why? For the same reason that the template also calls UseRouting(): to control exactly where in the pipeline the middleware is added.
As I mentioned in chapter 4, you can override the automatically added middleware by adding it yourself manually. It’s crucial that the authorization middleware be placed after the routing middleware, and as mentioned in chapter 4, you typically want to place your routing middleware after the static file middleware. As the routing middleware needs to move, so does the authorization middleware!
Traditionally, the authentication middleware is also placed after the routing middleware, before the authorization middleware, but this isn’t crucial. The only requirement is that it’s placed before any middleware that requires an authenticated user, such as the authorization middleware.
If you wish, you can move the location of the authentication middleware by calling UseAuthentication() at the appropriate point. I prefer to limit the work done on requests where possible, so I typically take this approach, moving it between the call to UseRouting() and UseAuthorization():
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
If you don’t place the authentication middleware at the correct point in the pipeline, you can run into strange bugs where users aren’t authenticated correctly or authorization policies aren’t applied correctly. The templates work out of the box, but you need to take care if you’re working with an existing application or moving middleware around.
Now that you’ve got an overview of the additions made by Identity, we’ll look in a bit more detail at the database schema and how Identity stores users in the database.
23.3.3 The ASP.NET Core Identity data model
Out of the box, and in the default templates, Identity uses EF Core to store user accounts. It provides a base DbContext that you can inherit from, called IdentityDbContext, which uses an IdentityUser as the user entity for your application.
In the template, the app’s DbContext is called ApplicationDbContext. If you open this file, you’ll see it’s sparse; it inherits from the IdentityDbContext base class I described earlier, and that’s it. What does this base class give you? The easiest way to see is to update a database with the migrations and take a look.
Applying the migrations is the same process as in chapter 12. Ensure that the connection string points to where you want to create the database, open a command prompt in your project folder, and run this command to update the database with the migrations:
dotnet ef database update
Tip If you see an error after running the dotnet ef command, ensure that you have the .NET tool installed by following the instructions provided in section 12.3.1. Also make sure that you run the command from the project folder, not the solution folder.
If the database doesn’t exist, the command-line interface (CLI) creates it. Figure 23.7 shows what the database looks like for the default template.
Tip If you’re using MS SQL Server (or LocalDB), you can use the SQL Server Object Explorer in Visual Studio to browse tables and objects in your database. See Microsoft’s “How to: Connect to a Database and Browse Existing Objects” article for details: http://mng.bz/mg8r.
Figure 23.7 The database schema used by ASP.NET Core Identity
That’s a lot of tables! You shouldn’t need to interact with these tables directly (Identity handles that for you), but it doesn’t hurt to have a basic grasp of what they’re for:
• __EFMigrationsHistory—The standard EF Core migrations table that records which migrations have been applied.
AspNetUsers—The user profile table itself. This is where IdentityUser is serialized to. We’ll take a closer look at this table shortly.
• AspNetUserClaims—The claims associated with a given user. A user can have many claims, so it’s modeled as a many-to-one relationship.
• AspNetUserLogins and AspNetUserTokens—These are related to third-party logins. When configured, these let users sign in with a Google or Facebook account (for example) instead of creating a password on your app.
• AspNetUserRoles, AspNetRoles, and AspNetRoleClaims—These tables are somewhat of a legacy left over from the old role-based permission model of the pre-.NET 4.5 days, instead of the claims-based permission model. These tables let you define roles that multiple users can belong to. Each role can be assigned multiple claims. These claims are effectively inherited by a user principal when they are assigned that role.
You can explore these tables yourself, but the most interesting of them is the AspNetUsers table, shown in figure 23.8.
Figure 23.8 The AspNetUsers table is used to store all the details required to authenticate a user.
Most of the columns in the AspNetUsers table are security-related—the user’s email, password hash, whether they have confirmed their email, whether they have MFA enabled, and so on. By default, there are no columns for additional information, like the user’s name.
NOTE You can see from figure 23.8 that the primary key Id is stored as a string column. By default, Identity uses Guid for the identifier. To customize the data type, see the “Change the primary key type” section of Microsoft’s “Identity model customization in ASP.NET Core” documentation: http://mng.bz/5jdB.
Any additional properties of the user are stored as claims in the AspNetUserClaims table associated with that user. This lets you add arbitrary additional information without having to change the database schema to accommodate it. Want to store the user’s date of birth? You could add a claim to that user; there’s no need to change the database schema. You’ll see this in action in section 23.6, when you add a Name claim to every new user.
NOTE Adding claims is often the easiest way to extend the default IdentityUser, but you can add properties to the IdentityUser directly. This requires database changes but is nevertheless useful in many situations. You can read how to add custom data using this approach here: http://mng.bz/Xd61.
It’s important to understand the difference between the IdentityUser entity (stored in the AspNetUsers table) and the ClaimsPrincipal, which is exposed on HttpContext.User. When a user first logs in, an IdentityUser is loaded from the database. This entity is combined with additional claims for the user from the AspNetUserClaims table to create a ClaimsPrincipal. It’s this ClaimsPrincipal that is used for authentication and is serialized to the authentication cookie, not the IdentityUser.
It’s useful to have a mental model of the underlying database schema Identity uses, but in day-to-day work, you shouldn’t have to interact with it directly. That’s what Identity is for, after all! In the next section we’ll look at the other end of the scale: the UI of the app and what you get out of the box with the default UI.
23.3.4 Interacting with ASP.NET Core Identity
You’ll want to explore the default UI yourself to get a feel for how the pieces fit together, but in this section I’ll highlight what you get out of the box, as well as areas that typically require additional attention right away.
The entry point to the default UI is the user registration page of the application, shown in figure 23.9. The register page enables users to sign up to your application by creating a new IdentityUser with an email and a password. After creating an account, users are redirected to a screen indicating that they should confirm their email. No email service is enabled by default, as this is dependent on your configuring an external email service. You can read how to enable email sending in Microsoft’s “Account confirmation and password recovery in ASP.NET Core” documentation at http://mng.bz/6gBo. Once you configure this, users will automatically receive an email with a link to confirm their account.
Figure 23.9 The registration flow for users using the default Identity UI. Users enter an email and password and are redirected to a “confirm your email” page. This is a placeholder page by default, but if you enable email confirmation, this page will update appropriately.
By default, user emails must be unique (you can’t have two users with the same email), and the password must meet various length and complexity requirements. You can customize these options and more in the configuration lambda of the call to AddDefaultIdentity() in Program.cs, as shown in the following listing.
Listing 23.2 Customizing Identity settings in ConfigureServices in Startup.cs
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true; #A
options.Lockout.AllowedForNewUsers = true; #B
options.Password.RequiredLength = 12; #C
options.Password.RequireNonAlphanumeric = false; #C
options.Password.RequireDigit = false; #C
})
.AddEntityFrameworkStores<AppDbContext>();
❶ Requires users to confirm their account by email before they can log in
❷ Enables user lockout, to prevent brute-force attacks against user passwords
❸ Updates password requirements. Current guidance is to require long passwords.
After a user has registered with your application, they need to log in, as shown in figure 23.10. On the right side of the login page, the default UI templates describe how you, the developer, can configure external login providers, such as Facebook and Google. This is useful information for you, but it’s one of the reasons you may need to customize the default UI templates, as you’ll see in section 23.5.
Figure 23.10 Logging in with an existing user and managing the user account. The Login page describes how to configure external login providers, such as Facebook and Google. The user-management pages allow users to change their email and password and to configure MFA.
Once a user has signed in, they can access the management pages of the identity UI. These allow users to change their email, change their password, configure MFA with an authenticator app, or delete all their personal data. Most of these functions work without any effort on your part, assuming that you’ve already configured an email-sending service.
That covers everything you get in the default UI templates. It may seem somewhat minimal, but it covers a lot of the requirements that are common to almost all apps. Nevertheless, there are a few things you’ll nearly always want to customize:
• Configure an email-sending service, to enable account confirmation and password recovery, as described in Microsoft’s “Account confirmation and password recovery in ASP.NET Core” documentation: http://mng.bz/vzy7.
• Add a QR code generator for the enable MFA page, as described in Microsoft’s “Enable QR Code generation for TOTP authenticator apps in ASP.NET Core” documentation: http://mng.bz/4Zmw.
• Customize the register and login pages to remove the documentation link for enabling external services. You’ll see how to do this in section 23.5. Alternatively, you may want to disable user registration entirely, as described in Microsoft’s “Scaffold Identity in ASP.NET Core projects” documentation: http://mng.bz/QmMG.
• Collect additional information about users on the registration page. You’ll see how to do this in section 23.6.
There are many more ways you can extend or update the Identity system and lots of options available, so I encourage you to explore Microsoft’s “Overview of ASP.NET Core authentication” at http://mng.bz/XdGv to see your options. In the next section you’ll see how to achieve another common requirement: adding users to an existing application.
23.4 Adding ASP.NET Core Identity to an existing project
In this section we’re going to add users to an existing application. The initial app is a Razor Pages app, based on recipe application from chapter 12. This is a working app that you want to add user functionality to. In chapter 24 we’ll extend this work to restrict control regarding who’s allowed to edit recipes on the app.
By the end of this section, you’ll have an application with a registration page, a login screen, and a manage account screen, like the default templates. You’ll also have a persistent widget in the top right of the screen showing the login status of the current user, as shown in figure 23.11.
Figure 23.11 The recipe app after adding authentication, showing the login widget
As in section 23.3, I’m not going to customize any of the defaults at this point, so we won’t set up external login providers, email confirmation, or MFA. I’m concerned only with adding ASP.NET Core Identity to an existing app that’s already using EF Core.
Tip It’s worth making sure you’re comfortable with the new project templates before you go about adding Identity to an existing project. Create a test app, and consider setting up an external login provider, configuring an email provider, and enabling MFA. This will take a bit of time, but it’ll be invaluable for deciphering errors when you come to adding Identity to existing apps.
To add Identity to your app, you’ll need to do the following:
- Add the ASP.NET Core Identity NuGet packages.
- Add the required Identity services to the dependency injection (DI) container.
- Update the EF Core data model with the Identity entities.
- Update your Razor Pages and layouts to provide links to the Identity UI.
This section tackles each of these steps in turn. At the end of section 23.4 you’ll have successfully added user accounts to the recipe app.
23.4.1 Configuring the ASP.NET Core Identity services
You can add ASP.NET Core Identity with the default UI to an existing app by referencing two NuGet packages:
• Microsoft.AspNetCore.Identity.EntityFrameworkCore—Provides all the core Identity services and integration with EF Core
• Microsoft.AspNetCore.Identity.UI—Provides the default UI Razor Pages
Update your project .csproj file to include these two packages:
<PackageReference
Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore"
Version="7.0.0" />
<PackageReference
Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.0" />
These packages bring in all the additional required dependencies you need to add Identity with the default UI. Be sure to run dotnet restore after adding them to your project.
Once you’ve added the Identity packages, you can update your Program.cs file to include the Identity services, as shown in the following listing. This is similar to the default template setup you saw in listing 23.1, but make sure to reference your existing AppDbContext.
Listing 23.3 Adding ASP.NET Core Identity services to the recipe app
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options => #A
options.UseSqlite(builder.Configuration #A
.GetConnectionString("DefaultConnection")!)); #A
builder.Services.AddDefaultIdentity<ApplicationUser>(options => #B
options.SignIn.RequireConfirmedAccount = true) #B
.AddEntityFrameworkStores<AppDbContext>(); #C
builder.Services.AddRazorPages();
builder.Services.AddScoped<RecipeService>();
❶ The existing service configuration is unchanged.
❷ Adds the Identity services to the DI container and uses a custom user type, ApplicationUser
❸ Makes sure you use the name of your existing DbContext app
This adds all the necessary services and configures Identity to use EF Core. I’ve introduced a new type here, ApplicationUser, which we’ll use to customize our user entity later. You’ll see how to add this type in section 23.4.2.
The next step is optional: add the AuthenticationMiddleware after the call to UseRouting() on WebApplication, as shown in the following listing. As I mentioned previously, the authentication middleware is added automatically by WebApplication, so this step is optional. I prefer to delay authentication until after the call to UseRouting(), as it eliminates the need to perform unnecessary work decrypting the authentication cookie for requests that don’t reach the routing middleware, such as requests for static files.
Listing 23.4 Adding AuthenticationMiddleware to the recipe app
app.UseStaticFiles(); #A
app.UseRouting();
app.UseAuthentication(); #B
app.UseAuthorization(); #C
app.MapRazorPages();
app.Run
❶ StaticFileMiddleware will never see requests as authenticated, even after you sign in.
❷ Adds AuthenticationMiddleware after UseRouting() and before UseAuthorization
❸ Middleware after AuthenticationMiddleware can read the user principal from HttpContext.User.
You’ve configured your app to use Identity, so the next step is updating EF Core’s data model. You’re already using EF Core in this app, so you need to update your database schema to include the tables that Identity requires.
23.4.2 Updating the EF Core data model to support Identity
The code in listing 23.3 won’t compile, as it references the ApplicationUser type, which doesn’t yet exist. Create the ApplicationUser in the Data folder, using the following line:
public class ApplicationUser : IdentityUser { }
It’s not strictly necessary to create a custom user type in this case (for example, the default templates use the raw IdentityUser), but I find it’s easier to add the derived type now rather than try to retrofit it later if you need to add extra properties to your user type.
In section 23.3.3 you saw that Identity provides a DbContext called IdentityDbContext, which you can inherit from. The IdentityDbContext base class includes the necessary DbSet
Updating an existing DbContext for Identity is simple: update your app’s DbContext to inherit from IdentityDbContext (which itself inherits from DbContext), as shown in the following listing. We’re using the generic version of the base Identity context in this case and providing the ApplicationUser type.
Listing 23.5 Updating AppDbContext to use IdentityDbContext
public class AppDbContext : IdentityDbContext<ApplicationUser> #A
{
public AppDbContext(DbContextOptions<AppDbContext> options) #B
: base(options) #B
{ } #B
public DbSet<Recipe> Recipes { get; set; } #B
}
❶ Updates to inherit from the Identity context instead of directly from DbContext
❷ The remainder of the class remains the same.
Effectively, by updating the base class of your context in this way, you’ve added a whole load of new entities to EF Core’s data model. As you saw in chapter 12, whenever EF Core’s data model changes, you need to create a new migration and apply those changes to the database.
At this point, your app should compile, so you can add a new migration called AddIdentitySchema using
dotnet ef migrations add AddIdentitySchema
The final step is updating your application’s Razor Pages and layouts to reference the default identity UI. Normally, adding 30 new Razor Pages to your application would be a lot of work, but using the default Identity UI makes it a breeze.
23.4.3 Updating the Razor views to link to the Identity UI
Technically, you don’t have to update your Razor Pages to reference the pages included in the default UI, but you probably want to add the login widget to your app’s layout at a minimum. You’ll also want to make sure that your Identity Razor Pages use the same base Layout.cshtml as the rest of your application.
We’ll start by fixing the layout for your Identity pages. Create a file at the “magic” path Areas/Identity/Pages/_ViewStart.cshtml, and add the following contents:
@{ Layout = "/Pages/Shared/_Layout.cshtml"; }
This sets the default layout for your Identity pages to your application’s default layout. Next, add a _LoginPartial.cshtml file in Pages/Shared to define the login widget, as shown in the following listing. This is pretty much identical to the template generated by the default template, but it uses our custom ApplicationUser instead of the default IdentityUser.
Listing 23.6 Adding a _LoginPartial.cshtml to an existing app
@using Microsoft.AspNetCore.Identity
@using RecipeApplication.Data; #A
@inject SignInManager<ApplicationUser> SignInManager #B
@inject UserManager<ApplicationUser> UserManager #B
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity"
asp-page="/Account/Manage/Index" title="Manage">
Hello @User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Page("/", new { area = "" })"
asp-area="Identity" method="post" >
<button class="nav-link btn btn-link text-dark"
type="submit">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity"
asp-page="/Account/Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity"
asp-page="/Account/Login">Login</a>
</li>
}
</ul>
❶ Updates to your project’s namespace that contains ApplicationUser
❷ The default template uses IdentityUser. Update to use ApplicationUser instead.
This partial shows the current login status of the user and provides links to register or sign in. All that remains is to render the partial by calling
<partial name="_LoginPartial" />
in the main layout file of your app, _Layout.cshtml.
And there you have it: you’ve added Identity to an existing application. The default UI makes doing this relatively simple, and you can be sure you haven’t introduced any security holes by building your own UI!
As I described in section 23.3.4, there are some features that the default UI doesn’t provide and you need to implement yourself, such as email confirmation and MFA QR code generation. It’s also common to find that you want to update a single page here and there. In the next section I’ll show how you can replace a page in the default UI, without having to rebuild the entire UI yourself.
23.5 Customizing a page in ASP.NET Core Identity’s default UI
In this section you’ll learn how to use scaffolding to replace individual pages in the default Identity UI. You’ll learn to scaffold a page so that it overrides the default UI, allowing you to customize both the Razor template and the PageModel page handlers.
Having Identity provide the whole UI for your application is great in theory, but in practice there are a few wrinkles, as you saw in section 23.3.4. The default UI provides as much as it can, but there are some things you may want to tweak. For example, both the login and register pages describe how to configure external login providers for your ASP.NET Core applications, as you saw in figures 23.12 and 23.13. That’s useful information for you as a developer, but it’s not something you want to be showing to your users. Another often-cited requirement is the desire to change the look and feel of one or more pages.
Luckily, the default Identity UI is designed to be incrementally replaceable, so you can override a single page without having to rebuild the entire UI yourself. On top of that, both Visual Studio and the .NET CLI have functions that allow you to scaffold any (or all) of the pages in the default UI so that you don’t have to start from scratch when you want to tweak a page.
DEFINITION Scaffolding is the process of generating files in your project that serve as the basis for customization. The Identity scaffolder adds Razor Pages in the correct locations so they override equivalent pages with the default UI. Initially, the code in the scaffolded pages matches that in the default Identity UI, but you are free to customize it.
As an example of the changes you can easily make, we’ll scaffold the registration page and remove the additional information section about external providers. The following steps describe how to scaffold the Register.cshtml page in Visual Studio:
-
Add the Microsoft.VisualStudio.Web.CodeGeneration.Design and Microsoft .EntityFrameworkCore.Tools NuGet packages to your project file, if they’re not already added. Visual Studio uses these packages to scaffold your application correctly, and without them you may get an error running the scaffolder:
<PackageReference Version="7.0.0" Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" /> <PackageReference Version="7.0.0" Include="Microsoft.EntityFrameworkCore.Tools" />
-
Ensure that your project builds. If it doesn’t build, the scaffolder will fail before adding your new pages.
-
Right-click your project, and choose Add > New Scaffolded Item from the contextual menu.
-
In the selection dialog box, choose Identity from the category, and choose Add.
-
In the Add Identity dialog box, select the Account/Register page, and select your application’s AppDbContext as the Data context class, as shown in figure 23.12. Choose Add to scaffold the page.
Figure 23.12 Using Visual Studio to scaffold Identity pages. The generated Razor Pages will override the versions provided by the default UI.
Tip To scaffold the registration page using the .NET CLI, install the required tools and packages as described in Microsoft’s “Scaffold Identity in ASP.NET Core projects” documentation: http://mng.bz/QPRv. Then run dotnet aspnet-codegenerator identity -dc RecipeApplication.Data.AppDbContext --files "Account.Register".
Visual Studio builds your application and then generates the Register.cshtml page for you, placing it in the Areas/Identity/Pages/Account folder. It also generates several supporting files, as shown in figure 23.13. These are required mostly to ensure that your new Register.cshtml page can reference the remaining pages in the default Identity UI.
Figure 23.13 The scaffolder generates the Register.cshtml Razor Page, along with supporting files required to integrate with the remainder of the default Identity UI.
We’re interested in the Register.cshtml page, as we want to customize the UI on the Register page, but if we look inside the code-behind page, Register.cshtml.cs, we see how much complexity the default Identity UI is hiding from us. It’s not insurmountable (we’ll customize the page handler in section 23.6), but it’s always good to avoid writing code if we can help it.
Now that you have the Razor template in your application, you can customize it to your heart’s content. The downside is that you’re now maintaining more code than you were with the default UI. You didn’t have to write it, but you may still have to update it when a new version of ASP.NET Core is released.
I like to use a bit of a trick when it comes to overriding the default Identity UI like this. In many cases, you don’t want to change the page handlers for the Razor Page—only the Razor view. You can achieve this by deleting the Register.cshtml.cs PageModel file, and pointing your newly scaffolded .cshtml file at the original PageModel, which is part of the default UI NuGet package.
The other benefit of this approach is that you can delete some of the other files that were autoscaffolded. In total, you can make the following changes:
• Update the @model directive in Register.cshtml to point to the default UI PageModel:
@model Microsoft.AspNetCore.Identity.UI.V5.Pages.Account.Internal.RegisterModel
• Update Areas/Identity/Pages/_ViewImports.cshtml to the following:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
• Delete Areas/Identity/Pages/_ValidationScriptsPartial.cshtml.
• Delete Areas/Identity/Pages/Account/Register.cshtml.cs.
• Delete Areas/Identity/Pages/Account/_ViewImports.cshtml.
After making all these changes, you’ll have the best of both worlds: you can update the default UI Razor Pages HTML without taking on the responsibility of maintaining the default UI code-behind.
Tip In the source code for the book, you can see these changes in action, where the Register view has been customized to remove the references to external identity providers.
Unfortunately, it’s not always possible to use the default UI PageModel. Sometimes you need to update the page handlers, such as when you want to change the functionality of your Identity area rather than only the look and feel. A common requirement is needing to store additional information about a user, as you’ll see in the next section.
23.6 Managing users: Adding custom data to users
In this section you’ll see how to customize the ClaimsPrincipal assigned to your users by adding claims to the AspNetUserClaims table when the user is created. You’ll also see how to access these claims in your Razor Pages and templates.
Often, the next step after adding Identity to an application is customizing it. The default templates require only an email and password to register. What if you need more details, like a friendly name for the user? Also, I’ve mentioned that we use claims for security, so what if you want to add a claim called IsAdmin to certain users?
You know that every user principal has a collection of claims, so conceptually, adding any claim requires adding it to the user’s collection. There are two main times that you would want to grant a claim to a user:
• For every user, when they register on the app—For example, you might want to add a Name field to the Register form and add that as a claim to the user when they register.
• Manually, after the user has registered—This is common for claims used as permissions, where an existing user might want to add an IsAdmin claim to a specific user after they have registered on the app.
In this section I’ll show you the first approach, automatically adding new claims to a user when they’re created. The latter approach is more flexible and ultimately is the approach many apps will need, especially line-of-business apps. Luckily, there’s nothing conceptually difficult to it; it requires a simple UI that lets you view users and add a claim through the same mechanism I’ll show here.
Tip Another common approach is to customize the IdentityUser entity, by adding a Name property, for example. This approach is sometimes easier to work with if you want to give users the ability to edit that property. Microsoft’s “Add, download, and delete custom user data to Identity in an ASP.NET Core project” documentation describes the steps required to achieve that: http://mng.bz/aoe7.
Let’s say you want to add a new Claim to a user, called FullName. A typical approach would be as follows:
- Scaffold the Register.cshtml Razor Page, as you did in section 23.5.
- Add a Name field to the InputModel in the Register.cshtml.cs PageModel.
- Add a Name input field to the Register.cshtml Razor view template.
- Create the new ApplicationUser entity as before in the OnPost() page handler by calling CreateAsync on UserManager
. - Add a new Claim to the user by calling UserManager.AddClaimAsync().
- Continue the method as before, sending a confirmation email or signing the user in if email confirmation is not required.
Steps 1–3 are fairly self-explanatory and require only updating the existing templates with the new field. Steps 4–6 take place in Register.cshtml.cs in the OnPostAsync() page handler, which is summarized in the following listing. In practice, the page handler has more error checking, boilerplate, extra features, and abstraction. I’ve simplified the code in listing 23.7 to focus on the additional lines that add the extra Claim to the ApplicationUser; you can find the full code in the sample code for this chapter.
Listing 23.7 Adding a custom claim to a new user in the Register.cshtml.cs page
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { #A
UserName = Input.Email, Email = Input.Email }; #A
var result = await _userManager.CreateAsync( #B
user, Input.Password); #B
if (result.Succeeded)
{
var claim = new Claim("FullName", Input.Name); #C
await _userManager.AddClaimAsync(user, claim); #D
var code = await _userManager #E
.GenerateEmailConfirmationTokenAsync(user); #E
await _emailSender.SendEmailAsync( #E
Input.Email, "Confirm your email", code ); #E
await _signInManager.SignInAsync(user); #F
return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors) #G
{ #G
ModelState.AddModelError( #G
string.Empty, error.Description); #G
} #G
}
return Page(); #G
}
❶ Creates an instance of the ApplicationUser entity
❷ Validates that the provided password meets requirements, and creates the user in the database
❸ Creates a claim, with a string name of “FullName” and the provided value
❹ Adds the new claim to the ApplicationUser’s collection
❺ Sends a confirmation email to the user, if you have configured the email sender
❻ Signs the user in by setting the HttpContext.User; the principal will include the custom claim
❼ There was a problem creating the user. Adds the errors to the ModelState and redisplays the page.
Tip Listing 23.7 shows how you can add extra claims at registration time, but you will often need to add more data later, such as permission-related claims or other information. You will need to create additional endpoints and pages for adding this data, securing the pages as appropriate (so that users can’t update their own permissions, for example).
This is all that’s required to add the new claim, but you’re not using it anywhere currently. What if you want to display it? Well, you’ve added a claim to the ClaimsPrincipal, which was assigned to the HttpContext.User property when you called SignInAsync. That means you can retrieve the claims anywhere you have access to the ClaimsPrincipal—including in your page handlers and in view templates. For example, you could display the user’s FullName claim anywhere in a Razor template with the following statement:
@User.Claims.FirstOrDefault(x=>x.Type == "FullName")?.Value
This finds the first claim on the current user principal with a Type of "FullName" and prints the assigned value (or, if the claim is not found, prints nothing). The Identity system even includes a handy extension method that tidies up this LINQ expression (found in the System.Security.Claims namespace):
@User.FindFirstValue("FullName")
With that last tidbit, we’ve reached the end of this chapter on ASP.NET Core Identity. I hope you’ve come to appreciate the amount of effort using Identity can save you, especially when you make use of the default Identity UI package.
Adding user accounts and authentication to an app is typically the first step in customizing your app further. Once you have authentication, you can have authorization, which lets you lock down certain actions in your app, based on the current user. In the next chapter you’ll learn about the ASP.NET Core authorization system and how you can use it to customize your apps; in particular, the recipe application, which is coming along nicely!
23.7 Summary
Authentication is the process of determining who you are, and authorization is the process of determining what you’re allowed to do. You need to authenticate users before you can apply authorization.
Every request in ASP.NET Core is associated with a user, also known as a principal. By default, without authentication, this is an anonymous user. You can use the claims principal to behave differently depending on who made a request.
The current principal for a request is exposed on HttpContext.User. You can access this value from your Razor Pages and views to find out properties of the user such as their, ID, name, or email.
Every user has a collection of claims. These claims are single pieces of information about the user. Claims could be properties of the physical user, such as Name and Email, or they could be related to things the user has, such as HasAdminAccess or IsVipCustomer.
Legacy versions of ASP.NET used roles instead of claims. You can still use roles if you need to, but you should typically use claims where possible.
Authentication in ASP.NET Core is provided by AuthenticationMiddleware and a number of authentication services. These services are responsible for setting the current principal when a user logs in, saving it to a cookie, and loading the principal from the cookie on subsequent requests.
The AuthenticationMiddleware is added automatically by WebApplication. You can ensure that it’s inserted at a specific point in the middleware pipeline by calling UseAuthentication(). It must be placed before any middleware that requires authentication, such as UseAuthorization().
ASP.NET Core Identity handles low-level services needed for storing users in a database, ensuring that their passwords are stored safely, and for logging users in and out. You must provide the UI for the functionality yourself and wire it up to the Identity subsystem.
The Microsoft.AspNetCore.Identity.UI package provides a default UI for the Identity system and includes email confirmation, MFA, and external login provider support. You need to do some additional configuration to enable these features.
The default template for Web Application with Individual Account Authentication uses ASP.NET Core Identity to store users in the database with EF Core. It includes all the boilerplate code required to wire the UI up to the Identity system.
You can use the UserManager<T>
class to create new user accounts, load them from the database, and change their passwords. SignInManager
You can update an EF Core DbContext to support Identity by deriving from IdentityDbContext
You can add additional claims to a user using the UserManager
Claims consist of a type and a value. Both values are strings. You can use standard values for types exposed on the ClaimTypes class, such as ClaimTypes.GivenName and ClaimTypes.FirstName, or you can use a custom string, such as "FullName".