Local Users with ASP.NET Core – ASP.NET Core Identity

Authentication and authorization is a built-in feature of ASP.NET Core. Creating an application, you can select to authenticate with the Azure Active Directory or the Azure Active Directory B2C, or store user information in a local database with the Web application. This article gives you the information to use local accounts, create roles programmatically from the application, and to restrict access to users belonging to specific roles.

Fingerprint authentication

Overview

Before storing users of your Web applications in a local database, think about other options available. Instead of the need to manage users in a local database, a better option is to have user information stored and managed independent of the application, e.g. by using Azure Active Directory or Azure Active Directory B2C. If the authentication service needs to run on-premises, consider using Identity Server. Identity Server is OpenID certified and part of the .NET Foundation.

In case you’ve reasons to store user information with the application in a SQL database, this feature is part of the ASP.NET Core experience as well. Let’s get into this.

Create the Application

Creating a new ASP.NET Core Web application, these are the options offered for authentication:

  • No Authentication
  • Individual User Accounts
  • Work or School Accounts
  • Windows Authentication

Using Work or School Accounts, Azure Active Directory can be selected. Individual User Accounts allows for the selection of Azure Active Directory B2C, or Store user accounts in-app which uses a local database to store the user accounts.

![https://csharpdotchristiannageldotcom.files.wordpress.com/2020/07/visualstudio2019storeuseraccountsinapp.png](Store user accounts in-app)

The connection string to the database is defined with appsettings.json with the DefaultConnection, using SQL Server localdb.

In the Startup class, the database is configured in the ConfigureServices method. Here, the ApplicationDbContext is configured as EF Core context to be used for the identity. The class ApplicationDbContext is defined with the created application and derives from the base class IdentityDbContext (namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore) to define all the tables and columns used for authentication and authorization.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddControllersWithViews();
    services.AddRazorPages();
}

In case you need to store additional data for your users, or adapt the information, the ApplicationDbContext is where you need to look at.

The Configure method in the Startup class configures middleware for authentication and authorizaton invoking the methods UseAuthentication and UseAuthorization.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });
}

Starting the application, you can already register a new user. If the database wasn’t created before, it is created using EF Core Migrations. You can start the database creation using the EF Core tools, or just start the application to register a user. Of course, you need to have the rights on your system to create the database. After creating of the application, SQL Server Express LocalDB is used. LocalDB is installed with Visual Studio.

Default User Interface

Where is the user interface for login and registration of the users coming from? It’s part of a library: Microsoft.AspNetCore.Identity.UI. This library contains Razor pages defining the user interface for identity. With the application created, you’ll find the Areas/Identity/Pages folder which contains the _ViewStart.cshtml file for this area. In this file you can see the assignment of the Layout property to reference the shared file _Layout.cshtml. This way the Razor pages from the library share the same layout definition with your application.

@{
    Layout = "/Views/Shared/_Layout.cshtml";
}

In case you need more customization to the user interface, you can add a new scaffoled item for Identity:

Scaffold Identity

With this option, you can select to override from all the different user interfaces that are part of this library to customize it for your application. Selecting the pages you want to customize, the Razor pages are created within the project, so you can easily customize it for your needs.

Scaffold Razor Pages for Identity

Verifying Emails

Registering a new account from the application succeeds after the database was created. However, this user cannot login. The reason is a default setting with the configuration of the default identity to require confirmed accounts. An easy way to change this behavior is to set the RequireConfirmedAccount to false. However, it’s not a lot more difficult to configure email validation. With the sample here, I’m using SendGrid to send emails. The SendGrid account can be created from the Microsoft Azure portal. Sending up to 25,000 emails per month is free with this Microsoft Azure offering.

To verify the emails, the interface IEmailSender needs to be implemented. This interface defines one method – SendEmailAsync. To send an email using SendGrid, the SendGrid NuGet package is added. This package contains the SendGridClient class that is used to send emails. After configuration, just the SendEmailAsync of the SendGridClient needs to be invoked.

public class EmailSender : IEmailSender
{
    private readonly EmailSenderOptions _options;
    public EmailSender(IOptions<EmailSenderOptions> options)
    {
        _options = options.Value;
    }

    public Task SendEmailAsync(string email, string subject, string htmlMessage)
    {
        var sendGridOptions = new SendGridClientOptions 
        {
             ApiKey = _options.ApiKey
        };
        var emailClient = new SendGridClient(sendGridOptions);
        var message = new SendGridMessage
        {
            From = new EmailAddress("authentication@sample.com"),
            Subject = subject,
            HtmlContent = htmlMessage
        };
        message.AddTo(email);
          
        return emailClient.SendEmailAsync(message);
    }
}

For the configuration, the API key for SendGrid is needed. This key is supplied with the class EmailSenderOptions:

public class EmailSenderOptions
{
    public string ApiKey { get; set; }
}

The key is configured with the user secrets of the application – during development.

{
  "SendGrid": {
    "ApiKey": "add your API key"
  }
}

The key can be created in the SendGrid portal that’s accessed from the Azure portal.

The EmailSender now needs to be configured with the DI container – in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.Configure<EmailSenderOptions>(Configuration.GetSection("SendGrid"));
    services.AddTransient<IEmailSender, EmailSender>();
}

With the EmailSender in place, a new account can be registered where an email is sent to verify the email address. After the email is clicked, the account is verified, and can successfully login to the Web application.

Features

The built-in authentication has support to reset the password via email, change the profile, and also supports the General Data Protection Regulation (GDPR). Clicking on the username, the user can download all his personal data stored, and delete the account.

Application Database

The created database contains these tables to manage users along with roles, claims, tokens…

  • AspNetUsers
  • AspNetUserLogins
  • AspNetUserClaims
  • AspNetUserTokens
  • AspNetRoles
  • AspNetUserRoles
  • AspNetRoleClaims

Managing Roles from the Application

For using roles, some configuration is required. Using AddDefaultIdentity to configure the DI container with identities, roles are not included by default. Instead, you can use the AddIdentity extension method which allows not only to pass the type of the user, but the type for the roles used, e.g.

services.AddIdentity<IdentityUser, IdentityRole>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
    })
    .AddDefaultUI()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

When using the method AddDefaultIdentity, the AddRoles method can be used to add role support as well.

To manage roles from the application, EF Core can be used to access the database via the ApplicationDbContext. However, there’s also an API which is more user friendly and independent of probably future database changes: RoleManager. RoleManager is a generic type which allows specifying the type to be used for the roles as a generic type parameter – IdentityRole. The constructor of the RoleManager needs IRoleStore, IRoleValidator, ILookupNormalizer, IdentityErrorDescriber, and ILogger parameters. Using dependency injection, all these paramters can be injected. To inject the RoleManager in a controller, just the extension method AddRoleManager needs to be invoked with the DI container. Similar to manage roles, managing users can be done with the UserManager (code file Startup.cs):

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddUserManager<UserManager<IdentityUser>>();

The controller where the RoleManager is injected is the RolesAdminController. This controller just shows all the roles available with the Index method, and the two Create methods to create a new role – using the CreateAsync method of the RoleManager passing an IdentityRole (code file Controllers/RolesAdminController.cs):

[Authorize(Roles = "Admins")]
public class RolesAdminController : Controller
{
    private readonly RoleManager<IdentityRole> _roleManager;
    public RolesAdminController(RoleManager<IdentityRole> roleManager)
    {
        _roleManager = roleManager;
    }

    public async Task<IActionResult> Index()
    {
        var identityRoles = await _roleManager.Roles.ToListAsync();
        return View(identityRoles);
    }

    public IActionResult Create()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create([Bind("Id, Name, NormalizedName")] IdentityRole role)
    {
        if (ModelState.IsValid)
        {
            await _roleManager.CreateAsync(role);

            return RedirectToAction(nameof(Index));
        }
        return View(role);
    }
}

In the Index view, the list of roles is shown (code file Views/RolesAdmin/Index.cshtml):

@foreach (var item in Model)
{
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Id)
        </td>
        <td>
            @Html.DisplayFor(model => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.NormalizedName)
        </td>
    </tr>
}

The Create view contains a HTML form to create new roles (code file Views/RolesAdmin/Create.cshtml):

<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

To programmatically assign a role to the current user, the UserManager can be used similar to the RoleManager. The method AddToRoleAsync requires an IdentityUser and the name of the role. The IdentityUser of the current user can be retrieved passing a ClaimsPrincipal to the GetUserMethod. The User property of the controllers base class returns the ClaimsPrincipal of the current user:

var claimsPrincipal = User;
var user = await userManager.GetUserAsync(claimsPrincipal);
var result = await userManager.AddToRoleAsync(user, role.Name);

Authentication with Roles

As roles are added to the AspNetRoles table, and mapping between users and roles is defined in the AspNetUserRoles table, requiring roles to invoke any action method of the controller can be applied with the Authorize attribute setting the Roles property. Applying this to the controller, every action method of the controller requires this authentication.

[Authorize(Roles = "Admins")]
public class RolesAdminController : Controller
{
    //...
}

Appling this attribute to the action method gives finer grained control. To programmatically check for a role, the IsInRole method of the ClaimsPrincipal can be used.

Take away

ASP.NET Core offers great built-in functionality to authenticate users from a local database. Checking for roles can be simple enabled with the identity configuration in the Startup class. With just a few lines of code, email verfication can be added. All the user interfaces for user registration, login, changing the password, downloading personal data… is included with a library – but can be completely customized with scaffolding. To manage users and roles programmatically, the UserManager and RoleManager APIs are available. Many features are available out of the box, easy to use, and completely customizable.

If you like this article, consider buying me a coffee which helps me staying up longer and writing more articles.

Buy Me A Coffee

You can get the complete sample code. See the RolesSample sample solution in the AspNetCore folder.

Enjoy learning and programming!

Christian

More Information

Account confirmation and password recovery

Identity Server

.NET Foundation

Entity Framework Core tools reference

Using Azure Active Directory B2C with ASP.NET Core

ASP.NET Core Identity Pages with ASP.NET Core 2.1

More information on C# and programming .NET Core applications is in my book Professional C# 7 and .NET Core 2.0, and in my workshops.

Fingerprint authentication image ID 188969170 © Serhii Yaremenko | Dreamstime.com

5 thoughts on “Local Users with ASP.NET Core – ASP.NET Core Identity

  1. Christian I loved this article.
    I have a question though, I understand with scaffolding we can completely customize these UI behaviors. However I’m curious to know if there’s a way to customize the behaviors of AccountController as well. For example what if I need to capture additional information during registration and update another table or what if I need to completely remove self registration. Although we can modify/remove the views these functions would still be available at the controller level, that’s why I want to remove them from Account Controller.

    Liked by 1 person

    1. It’s not just the UI that’s generated, there are the complete Razor pages available. The Identity views are not built with MVC but with Razor pages – and you can customize these completely 🙂

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.