Exercise - Configure multi-factor authentication

Completed

In the previous unit, you learned how ASP.NET Core Identity implements time-based one-time password (TOTP) for multi-factor authentication (MFA). In this unit, you customize the existing Configure authenticator app form to provide a QR code that contains the registration key.

Generating QR codes

Multiple strategies exist for generating the QR code. An example in the documentation uses a client-side JavaScript library. In this unit, however, a third-party NuGet package is used to generate the QR code with C# on the server. The resulting QR code image is injected into an HTML placeholder element as a base-64 encoded string.

Add a QR code service

Let's build everything you need to generate QR codes on the Configure authenticator app form.

  1. In the terminal pane, install the QRCoder NuGet package:

    dotnet add package QRCoder --version 1.4.3
    
  2. In the Explorer pane, right-click on the Services folder and add a new file named QRCodeService.cs. Add the following code:

    using QRCoder;
    
    namespace RazorPagesPizza.Services;
    public class QRCodeService
    {
        private readonly QRCodeGenerator _generator;
    
        public QRCodeService(QRCodeGenerator generator)
        {
            _generator = generator;
        }
    
        public string GetQRCodeAsBase64(string textToEncode)
        {
            QRCodeData qrCodeData = _generator.CreateQrCode(textToEncode, QRCodeGenerator.ECCLevel.Q);
            var qrCode = new PngByteQRCode(qrCodeData);
    
            return Convert.ToBase64String(qrCode.GetGraphic(4));
        }
    }
    

    The preceding code:

    • Uses constructor injection to gain access to an instance of the library's QRCodeGenerator class.
    • Exposes the GetQRCodeAsBase64 method to return the base-64 encoded string. The QR code dimensions are determined by the integer value passed to GetGraphic. In this case, the generated QR code is composed of blocks sized four pixels squared.
  3. In Program.cs, add the highlighted lines:

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    using Microsoft.AspNetCore.Identity.UI.Services;
    using RazorPagesPizza.Services;
    using QRCoder;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    builder.Services.AddTransient<IEmailSender, EmailSender>();
    builder.Services.AddSingleton(new QRCodeService(new QRCodeGenerator()));
    
    var app = builder.Build();
    

    QRCodeService is registered as a singleton service in the IoC container within Program.cs.

Customize multi-factor authentication

Now that you can generate QR codes, you can embed a QR code into the Configure authenticator app form.

  1. Open Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs and make the following changes:

    1. Add the following property to the EnableAuthenticatorModel class to store the QR code's base-64 string representation:

      public class EnableAuthenticatorModel : PageModel
      {
          private readonly UserManager<RazorPagesPizzaUser> _userManager;
          private readonly ILogger<EnableAuthenticatorModel> _logger;
          private readonly UrlEncoder _urlEncoder;
      
          public string QrCodeAsBase64 { get; set; }    
      
    2. Incorporate the highlighted changes in the OnGetAsync page handler:

      public async Task<IActionResult> OnGetAsync([FromServices] QRCodeService qrCodeService)
      {
          var user = await _userManager.GetUserAsync(User);
          if (user == null)
          {
              NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
          }
      
          await LoadSharedKeyAndQrCodeUriAsync(user);
          QrCodeAsBase64 = qrCodeService.GetQRCodeAsBase64(AuthenticatorUri);
      
          return Page();
      }
      

      In the preceding page handler, parameter injection provides a reference to the QRCodeService singleton service.

    3. Add the following using statement to the top of the file to resolve the reference to QRCodeService. Save your changes.

      using RazorPagesPizza.Services;
      
    4. Incorporate the highlighted change to the GenerateQrCodeUri method.

      private string GenerateQrCodeUri(string email, string unformattedKey)
      {
          return string.Format(
              CultureInfo.InvariantCulture,
              AuthenticatorUriFormat,
              _urlEncoder.Encode("RazorPagesPizza"),
              _urlEncoder.Encode(email),
              unformattedKey);
      }
      

      This sets the display name for the key in your TOTP app.

  2. In Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml, make the following highlighted changes and save:

    <li>
        <p>Scan the QR Code or enter this key <kbd>@Model.SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
        <div id="qrCode">
            <img alt="embedded QR code" src="data:image/png;base64,@Model.QrCodeAsBase64" />
        </div>
        <div id="qrCodeData" data-url="@Model.AuthenticatorUri"></div>
    </li>
            
    

    The preceding markup embeds the base-64 encoded image in the page.

Test multi-factor authentication

You've made all the changes needed for a QR code on the Configure authenticator app form. Now you can easily test the MFA functionality.

  1. Ensure that you've saved all your changes.

  2. Build and run the app with dotnet run.

  3. Navigate to the site and sign in with either registered user, if you're not already signed in. Select Hello, [First name] [Last name]! link to navigate to the profile management page, and then select Two-factor authentication.

  4. Select the Add authenticator app button.

  5. Follow the on-screen instructions to register and verify your authenticator app for this user.

    For example, using Microsoft Authenticator on Android, follow these steps to add the account to the app:

    1. Open the Microsoft Authenticator app.
    2. Select the kebab menu (vertical ellipsis) in the upper right.
    3. Select Add account.
    4. Select Other account (Google, Facebook, etc.).
    5. Scan the QR code as indicated.
  6. Enter the verification code provided by your TOTP app in the Verification Code text box.

  7. Select Verify.

    Upon successful verification, the page displays a Your authenticator app has been verified banner and some recovery codes.

  8. In the SQL Server tab in VS Code, right-click the RazorPagesPizza database and select New query. Enter the following query and press Ctrl+Shift+E to run it.

    SELECT FirstName, LastName, Email, TwoFactorEnabled
    FROM dbo.AspNetUsers
    

    For the signed in user, the output shows that the TwoFactorEnabled column is equal to 1. Because multi-factor authentication hasn't been enabled for the other registered user, the record's column value is 0.

  9. In the web app, select Logout, and then sign in again with the same user.

  10. Enter the verification code from the TOTP authenticator app in the Authenticator code text box. Select the Log in button.

  11. Select Hello, [First name] [Last name]!. Then, select the Two-factor authentication tab.

    Because Microsoft Authenticator has been set up, the following buttons appear:

    • Disable 2FA
    • Reset recovery codes
    • Set up authenticator app
    • Reset authenticator app
  12. In the terminal pane in VS Code, press Ctrl+C to stop the app.

Summary

In this unit, you added the ability to generate a QR code to the Configure authenticator app form. In the next unit, you can learn about using Identity to store claims and apply authorization policies.