Core Identity: Enforcing 2FA Setup on Login

Implementing two/multi-factor authentication (2FA/MFA) with the ASP.NET Core Identity Framework is not difficult, especially if users can decide for themselves whether and when to set up two-factor authentication. It becomes tricky when user accounts are created by third parties (e.g. administrators) and the users are required to set up their second factor, e.g. a TOTP authenticator app, before using the system. This article describes a possible solution for this scenario. It uses only public interfaces and standard concepts from Identity Core, so that the code is future-proof and does not interfere with the inner workings of the framework. Although the standard Identity Core example is used for the demonstration, the individual steps should also be easy to reproduce in an existing application. The scenario and the requirements are described below. This allows you to check whether this article can meet your expectations. After that, the solution strategy is described and the implementation is shown. Scenario / Requirements A web application is to be protected with the help of Identity Core. As it is an enterprise application that has to be operated and maintained for several years, it is essential that only public interfaces and standard concepts of the framework are used. The application should be able to be used by three different types of users: Anonymous users: Can access the homepage, help pages and login page. Registered users (legacy): May access the protected area. As their accounts were set up before the introduction of MFA, they do not have to use it to sign in. However, they can set up and use MFA if they wish. Registered users (standard): May access the protected area. As their accounts were set up after the introduction of MFA, they are forced to use MFA. Users cannot register themselves. A user account is created by administrators. There is no differentiation of login methods within the protected area. There are therefore no HTTP endpoints that are only accessible in the MFA case. Solution strategy As the administrators do not have access to the users' Authenticator app during setup, they cannot set up 2FA in its full extent. Therefore, standard users must configure 2FA the first time they access the protected area. The types of users mentioned above result in the following four logical types of user accounts: Legacy user account without 2FA Legacy user account with 2FA Standard user account without completed setup Standard user account with completed setup Core Identity does not recognize different types of user accounts. Therefore, the user account will have to be extended by a property that can be used to differentiate between legacy and standard user accounts. The standard implementation of the login process of Core Identity automatically ensures that 2FA is used if this has been set up correctly for the user account. In the case of type 3 user accounts, the user must be immediately redirected to the 2FA setup page after the password-based login. Forwarding to the 2FA setup page ensures that users are forwarded to the next step of the workflow, but access to the protected area is possible because the user is correctly logged in from Core Identity's point of view. To prevent this, each request must be examined to ensure that the signed-in user has used the authentication procedure permitted for them. This can be implemented with the help of a middleware. This allows all requests to pass, except for those from user accounts of type 3. These requests are checked and, if necessary, redirected to the 2FA setup page. Implementation Preparation As already mentioned in the introduction, the standard example from Identity Core is to be used here. To create the corresponding code, the following command must be executed on the CLI in an empty directory: dotnet new webapp --auth Individual -o WebApp1 The code of the front end is not generated by default. To do this, the ASP.NET Core Staffolder is required: dotnet tool install -g dotnet-aspnet-codegenerator Switch to the directory WebApp1 and add a few more packages: dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.AspNetCore.Identity.UI dotnet add package Microsoft.EntityFrameworkCore.Tools Code for the missing pages can then be generated: dotnet aspnet-codegenerator identity -fi Account.Login;Account.LoginWith2fa;Account.Register;Account.Manage.EnableAuthenticator -dbProvider sqlite Database The scaffolder creates a DbContext that is not used and can lead to issues. Look for the file Areas/Identity/Data/WebApp1IdentityDbContext.cs and delete it. Open Program.cs and remove the unused and unresolvable import of WebApp1.Areas.Identity.Data. To initialize the database, exe

Apr 13, 2025 - 17:42
 0
Core Identity: Enforcing 2FA Setup on Login

Implementing two/multi-factor authentication (2FA/MFA) with the ASP.NET Core Identity Framework is not difficult, especially if users can decide for themselves whether and when to set up two-factor authentication.

It becomes tricky when user accounts are created by third parties (e.g. administrators) and the users are required to set up their second factor, e.g. a TOTP authenticator app, before using the system.

This article describes a possible solution for this scenario. It uses only public interfaces and standard concepts from Identity Core, so that the code is future-proof and does not interfere with the inner workings of the framework.

Although the standard Identity Core example is used for the demonstration, the individual steps should also be easy to reproduce in an existing application.

The scenario and the requirements are described below. This allows you to check whether this article can meet your expectations. After that, the solution strategy is described and the implementation is shown.

Scenario / Requirements

A web application is to be protected with the help of Identity Core. As it is an enterprise application that has to be operated and maintained for several years, it is essential that only public interfaces and standard concepts of the framework are used.

The application should be able to be used by three different types of users:

  • Anonymous users: Can access the homepage, help pages and login page.
  • Registered users (legacy): May access the protected area. As their accounts were set up before the introduction of MFA, they do not have to use it to sign in. However, they can set up and use MFA if they wish.
  • Registered users (standard): May access the protected area. As their accounts were set up after the introduction of MFA, they are forced to use MFA.

Users cannot register themselves. A user account is created by administrators.

There is no differentiation of login methods within the protected area. There are therefore no HTTP endpoints that are only accessible in the MFA case.

Solution strategy

As the administrators do not have access to the users' Authenticator app during setup, they cannot set up 2FA in its full extent. Therefore, standard users must configure 2FA the first time they access the protected area.

The types of users mentioned above result in the following four logical types of user accounts:

  1. Legacy user account without 2FA
  2. Legacy user account with 2FA
  3. Standard user account without completed setup
  4. Standard user account with completed setup

Core Identity does not recognize different types of user accounts. Therefore, the user account will have to be extended by a property that can be used to differentiate between legacy and standard user accounts.

The standard implementation of the login process of Core Identity automatically ensures that 2FA is used if this has been set up correctly for the user account. In the case of type 3 user accounts, the user must be immediately redirected to the 2FA setup page after the password-based login.

Forwarding to the 2FA setup page ensures that users are forwarded to the next step of the workflow, but access to the protected area is possible because the user is correctly logged in from Core Identity's point of view.

To prevent this, each request must be examined to ensure that the signed-in user has used the authentication procedure permitted for them. This can be implemented with the help of a middleware. This allows all requests to pass, except for those from user accounts of type 3. These requests are checked and, if necessary, redirected to the 2FA setup page.

Implementation

Preparation

As already mentioned in the introduction, the standard example from Identity Core is to be used here. To create the corresponding code, the following command must be executed on the CLI in an empty directory:

dotnet new webapp --auth Individual -o WebApp1

The code of the front end is not generated by default. To do this, the ASP.NET Core Staffolder is required:

dotnet tool install -g dotnet-aspnet-codegenerator

Switch to the directory WebApp1 and add a few more packages:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.Tools

Code for the missing pages can then be generated:

dotnet aspnet-codegenerator identity -fi Account.Login;Account.LoginWith2fa;Account.Register;Account.Manage.EnableAuthenticator -dbProvider sqlite

Database

The scaffolder creates a DbContext that is not used and can lead to issues. Look for the file Areas/Identity/Data/WebApp1IdentityDbContext.cs and delete it.

Open Program.cs and remove the unused and unresolvable import of WebApp1.Areas.Identity.Data.

To initialize the database, execute the following command:

dotnet ef database update

Differentiation between legacy and standard user accounts

The solution strategy requires a differentiation between legacy user accounts (2FA is voluntary) and standard user accounts (2FA is mandatory). As a result, this property must be assigned during user registration.

Note: This article is about showing the technical solution. For better readability, therefore, no administration API is implemented, but the existing "Register User" form is regarded as the administrator interface.

In order to be able to create both types of user account for test purposes, a checkbox is added to the register form, which can be used to differentiate between legacy and standard users.

Open Register.cshtml and add a checkbox below the second password field:

 class="form-check mb-3">
   asp-for="Input.RequireMfa" class="form-check-input" type="checkbox" />
   asp-for="Input.RequireMfa" class="form-check-label">Create standard user-account

And add a property to the model (Register.cshtml.cs):

public class InputModel
{
    /// 
    /// Indicates whether the user is required to use 2FA/MFA
    /// 
    public bool RequireMfa { get; set; }
}

The method that processes the input must be adapted so that the type of user account is saved.
Make sure that the user was created. Use the log-message "User created a new account with password." as an anchor. Add a corresponding claim to the user and update the identity-session to make the change available immediately:

_logger.LogInformation("User created a new account with password.");

await _userManager.AddClaimAsync(user, new("enforceMfa", Input.RequireMfa ? "true" : "false"));
await _signInManager.RefreshSignInAsync(user);

The use of a claim to flag user accounts may seem odd. However, it offers two advantages:
There is no need to create a separate user account type (derived from IdentityUser). This means that persistence is completely taken over by Core Identity or its Entity Framework integration. In addition, the claim is automatically transferred to the claims of the principal in the identity session. This means that the value can later be accessed directly in the middleware without the need for database access.

Modify login logic

Standard users without configured 2FA should be redirected to the setup page. A redirect may therefore need to be made after a login with a password. For this purpose, the logic in Login.cshtml.cs after a user has logged in needs to be extended.

Use the log message "User logged in." as anchor:

_logger.LogInformation("User logged in.");

var user = await _userManager.FindByNameAsync(Input.Email);
var claims = await _userManager.GetClaimsAsync(user);
var claim = claims.FirstOrDefault(c => c.Type == "enforceMfa");
var mfaIsRequired = claim is not null && claim.Value == "true";
if (mfaIsRequired)
{
  // MFA is required but was not used to log in. Redirect user to setup.
  return RedirectToPage("./Manage/EnableAuthenticator");
}
else
{
  return LocalRedirect(returnUrl);
}

The default implementation does not use a UserManager. Add the required dependency using constructor-injection:

public class LoginModel : PageModel
{
    private readonly UserManager<IdentityUser> _userManager;

    public LoginModel(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, ILogger<LoginModel> logger)
    {
        _userManager = userManager;
    }

Simplify Authenticator Setup

To be able to conveniently set up the authenticator app later during testing, a corresponding QR code must be generated. To generate this on the server side, an additional package is required:

dotnet add package QrCoder

Add a property to the model EnableAuthenticatorModel, which will contain the QR code as a Base64-encoded PNG:

/// 
/// QR code for authenticator setup encoded as base64 PNG
/// 
public string AuthenticatorQrCodeAsBase64 { get; set; }

Add QR code generation to LoadSharedKeyAndQrCodeUriAsync just below the AuthenticatorUri-assignment.

AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey);

var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(AuthenticatorUri, QRCodeGenerator.ECCLevel.Q);
var qrCode = new PngByteQRCode(qrCodeData);
AuthenticatorQrCodeAsBase64 = Convert.ToBase64String(qrCode.GetGraphic(5));

To display the QR code, change the page EnableAuthenticator.cshtml to display the image in div-block qrCode.

 id="qrCode"> src="data:image/png;base64,@Model.AuthenticatorQrCodeAsBase64" alt="QR Code" />