fbpx

07. IdentityServer4 MFA – TOTP

You can find the project here.

Why MFA

Multi-factor authentication or MFA requires multiple factors to authenticate a user. Two-factor authentication (2FA) is an MFA with two factors. There is no real limit on how many factors we can add but it’s not practical for a user to use many factors as it hurts usability of the application if the login process is too long and complicated. Adding a second factor is usually enough to stop the brute-force and dictionary attacks.

Not all MFA is created equal. Some factors are still vulnerable to reverse proxy scenarios where a phishing attack is launched to trick users into clicking a malicious link that will mimic a real application look and feel using a reverse proxy and will allow attackers to capture username, password and any type of code entered as a second factor. Other factors like hardware-based authenticators (think YubiKey 5 for example) using FIDO2 will protect you against reverse proxy man-in-the-middle attack scenarios. It all boils down to making trade-offs between usability, price, and security. Adding a second factor, even if it’s not an optimal one, is still better than having a single factor. There are only 3 categories of things that can be used to authenticate you:

  1. Something you know (password, PIN, gesture, etc.)
  2. Something you have (a card, key, one-time password, some kind of device, etc.)
  3. Something you are (face, fingerprint, retina, typing style, etc.)

Good authentication flow will use multiple factors from multiple categories. It might use a password and a one-time password (2FA with TOTP Google Authenticator for example), card and a PIN (ATM for example), face and password (on your phone for example).

Legacy ways of implementing MFA by sending codes using email or SMS are not recommended anymore as they are considered not secure enough as those channels can be easily intercepted. Time-based One-time Password (TOTP) is considered more secure with code being automatically generated every 30 seconds without the server and TOTP app talking to each other. It is based on a timestamp and TOTP algorithm. TOTP is generally accepted as a minimum these days to implement MFA.

An even better way of doing it is using FIDO2 hardware authenticators like YubiKey 5, SoloKeys, etc based on RSA encryption with public/private key pairs. There is currently no known way to bypass hardware authenticators with phishing or man-in-the-middle attacks or using social engineering approach. The only way would be for someone to steal your key (it is a small device like a USB dongle).

IdentityServer4, as we previously learned, has nothing to do with users and doesn’t care much about them. That is why we implemented the ASP.NET Core Identity as our user store. Along with user data storage, we got a handful of useful methods to deal with registering users, setting the password and adding additional factors. Our focus is to actually extend the ASP.NET Core Identity to work with the desired factors. We will only use factors that are considered secure in 2019 and avoid any legacy ways of doing MFA.

The first way we will implement MFA is using TOTP with Google Authenticator (or any other standard TOTP authenticator app) and the second way is using FIDO2 with YubiKey 5 (we will add FIDO2 in my next tutorial). Let’s get to it.

 

Code changes

This tutorial is based on a project from a previous tutorial. Scaffolded ASP.NET Core Identity comes with TOTP support out of the box. We just need to add some missing bits and pieces to our IdentityServer4 “Quickstart” to make it work properly.

The easiest way to connect to the TOTP authenticator app (Google Authenticator for example) is by scanning the QR code. We need to add the JavaScript library that will generate the QR code for us. Download the qrcode.js library from here https://davidshimjs.github.io/qrcodejs/ and put “qrcode.min.js” into “wwwroot/lib/qrcode” folder like so:

Now let’s open “Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml” and modify the “Scripts” section at the end of the file like so:

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
    <script type="text/javascript" src="~/lib/qrcode/qrcode.min.js"></script>
    <script type="text/javascript">
        new QRCode(document.getElementById("qrCode"),
            {
                text: "@Html.Raw(Model.AuthenticatorUri)",
                width: 150,
                height: 150
            });
    </script>
}

Comment out (or remove) the section of the page that shows how to implement the QR code as we already implemented it like so:

@*<div class="alert alert-info">To enable QR code generation please read our <a href="https://go.microsoft.com/fwlink/?Linkid=852423">documentation</a>.</div>*@

In order for local login to allow user to enter TOTP we need to modify the response after successful first factor authentication with username and password. Open “Quickstart/Account/AccountController.cs” and locate HttpPost “Login” method. We will not get the “result.Succeeded” result anymore but “result.RequiresTwoFactor” result after the 2FA is enabled for a user. Let’s modify the code to handle this scenario like so:

if (result.Succeeded)
{
	(existing code - do not change)
}
else if (result.RequiresTwoFactor)
{
	string twoFactorUrl = "~/Identity/Account/LoginWith2fa?ReturnUrl={0}";
	if (context != null || Url.IsLocalUrl(model.ReturnUrl))
	{
		return Redirect(string.Format(twoFactorUrl, HttpUtility.UrlEncode(model.ReturnUrl)));
	}
	else
	{
		return Redirect(string.Format(twoFactorUrl, HttpUtility.UrlEncode("~/")));
	}
}

That is all that is pretty much needed to get the TOTP to work and show with QR code. To get the full experience however we need to make a couple of changes to get the scaffolded ASP.NET Core Identity to work with us. To prove my point, start the application and navigate to http://localhost:5000/Identity/Account/Manage.

You will see an error like so:

ASP.NET Core Identity needs the registered implementation of IEmailSender to resolve the missing service. I will use SendGrid as an example, you can change it to whatever suits you.

First, install the “Sendgrid” NuGet package like so:

Next in the “Services” folder add a new class and name it “EmailSender”. It will implement the “IEmailSender” interface like so:

using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Configuration;
using SendGrid;
using SendGrid.Helpers.Mail;
using System.Threading.Tasks;

namespace IdentityServer.Services
{
    public class EmailSender : IEmailSender
    {
        private readonly IConfiguration _configuration;

        public EmailSender(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public Task SendEmailAsync(string email, string subject, string message)
        {
            string key = _configuration.GetSection("SendGrid").GetValue<string>("ApiKey");
            return Execute(key, subject, message, email);
        }

        public Task Execute(string apiKey, string subject, string message, string email)
        {
            var client = new SendGridClient(apiKey);
            string fromEmail = _configuration.GetSection("SendGrid").GetValue<string>("FromEmail");
            string fromName = _configuration.GetSection("SendGrid").GetValue<string>("FromName");
            
            var msg = new SendGridMessage()
            {
                From = new EmailAddress(fromEmail, fromName),
                Subject = subject,
                PlainTextContent = message,
                HtmlContent = message
            };
            msg.AddTo(new EmailAddress(email));

            // Disable click tracking.
            // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
            msg.SetClickTracking(false, false);

            return client.SendEmailAsync(msg);
        }
    }
}

Let’s add configuration for the SendGrid in “appsettings.json” like so:

"SendGrid": {
	"ApiKey": "xxxxxxxx",
	"FromEmail": "identity@server.com",
	"FromName": "Identity Server"
}

Note: Change the SendGrid settings in “appsettings.json” to reflect your SendGrid config.

Let’s register the service in “Startup.cs”. Find the “ConfigureServices” method and at the end of the method add

services.AddTransient<IEmailSender, EmailSender>();

To make our lives a bit easier later let’s add these razor page options to the start of the “ConfigureServices” method like so:

services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
	options.AllowAreas = true;
	options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
});

Add the missing “using” directive:

using Microsoft.AspNetCore.Identity.UI.Services;

Now we have the “EmailSender” registered and ready to send email using SendGrid.

There is a known bug with ASP.NET Core Identity scaffolding process where one razor page called “ShowRecoveryCodes” is not scaffolded so it’s missing in the project. We need to manually add this razor page in as we will need it later on. Open folder “Areas/Identity/Pages/Account/Manage” and add new “Razor Page” item with name “ShowRecoveryCodes”. It will create two files. One is the razor view itself “ShowRecoveryCodes.cshtml” and another is code-behind file “ShowRecoveryCodes.cshtml.cs”.

Modify “ShowRecoveryCodes.cshtml” like so:

@page
@model ShowRecoveryCodesModel
@{
    ViewData["Title"] = "Recovery codes";
    ViewData["ActivePage"] = "TwoFactorAuthentication";
}

<partial name="_StatusMessage" for="StatusMessage" />
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
    <p>
        <span class="glyphicon glyphicon-warning-sign"></span>
        <strong>Put these codes in a safe place.</strong>
    </p>
    <p>
        If you lose your device and don't have the recovery codes you will lose access to your account.
    </p>
</div>
<div class="row">
    <div class="col-md-12">
        @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2)
        {
            <code class="recovery-code">@Model.RecoveryCodes[row]</code><text> </text><code class="recovery-code">@Model.RecoveryCodes[row + 1]</code><br />
        }
    </div>
</div>

Next modify the “ShowRecoveryCodes.cshtml.cs” like so:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IdentityServer.Areas.Identity.Pages.Account.Manage
{
    /// <summary>
    ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
    ///     directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public class ShowRecoveryCodesModel : PageModel
    {
        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [TempData]
        public string[] RecoveryCodes { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        [TempData]
        public string StatusMessage { get; set; }

        /// <summary>
        ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
        ///     directly from your code. This API may change or be removed in future releases.
        /// </summary>
        public IActionResult OnGet()
        {
            if (RecoveryCodes == null || RecoveryCodes.Length == 0)
            {
                return RedirectToPage("./TwoFactorAuthentication");
            }

            return Page();
        }
    }
}

Let’s do some cosmetic changes to align the ASP.NET Core Identity layout with the IdentityServer4 layout.

Open “Areas/Identity/Pages/Account/Manage/_Layout.cshtml” and change the master layout like so.

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

Final change is to create “_ViewStart.cshtml” in same folder “Areas/Identity/Pages/Account/Manage” like so:

@{
    Layout = "_Layout.cshtml";
}

Done.

 

TOTP Enrollment process

Launch IdentityServer4 application and navigate to http://localhost:5000/Identity/Account/Manage. You should see it now working in full glory

Click on “Two-factor authentication”.

Click on the “Add authenticator app”. You should now see the generated QR code.

On your mobile device install the “Google Authenticator” application (or any other TOTP authenticator app). Start the app and tap on “Scan a barcode”

You should get the code generated on your screen like so:

Enter the code on the page and click “Verify”. An authenticator is now verified. You can see the recovery codes. Save them as a backup in case you lose your phone or else you will not be able to authenticate.

Tap on “Add account” on the authenticator app to confirm you want to add the account.

Done.

 

Take a look at the user tables

Take a look at these tables “dbo.AspNetUsers” and “dbo.AspNetUserTokens”. You will see that the “TwoFactorEnabled” flag is turned on for user “alice” (that is the user I used). You will also notice that two users tokens got created for the same user “alice”. One is the authenticator key itself and another one is for the recovery codes.

Play with it

Sign out, sign in, reset authenticator app, disable 2FA. Play with it and get to know it good as we are going to extend this part of the ASP.NET Core Identity to add FIDO2 in my next tutorial. Because we want all them factors!

You can find the project here.

Support

For direct assistance schedule a technical meeting with Ivan to talk about your requirements.
For a general overview of our services and a live demo schedule a meeting with Maja.

Comments
  • Venkat says:

    Hello,
    We are testing the product in our test environment.
    “Remember Machine” check in Authenticator Code Verify Screen is working on my local machine but not working in integrated aws environment.
    May I know how and where its value being managed? If I have my app setup in AWS do I need to do special care?
    Thanks!

    • deblokt says:

      Hi,
      Could you please provide more context as I don’t follow. Which Authenticator Code Verify Screen? For TOTP you just need to enter TOTP code from your Authenticator, there is no option to “Rember Machine” on the page itself. Thanks

  • Terry says:

    I cannot get the http://localhost:5000/Identity/Account/Manage page to display. The login, logout, diagnostics, etc pages show but I get the error: “This localhost page can’t be foundNo webpage was found for the web address: http://localhost:5000/Identity/Account/Manage
    HTTP ERROR 404″

    for the Manage page. I don’t see a view or controller actions for the Manage page in the source repo either.

    Thanks for posting these tutorials. They are helpful integrating your Identity4 server and ASP.NET Core 3.1 Identity user stores.

  • Hari says:

    Hi,
    This is really good, Can you please confirm below as well?
    1) Do we need license to use Google Authenticator for enterprise?
    2) Where can i set up domain/issuer for otpauth://totp/? (looks you are using “IdentityServer”)
    3) Looks we need to send email address to get code, will they store email some where?

    Thanks,

    • deblokt says:

      Hi,

      1. I don’t think so but please check with Google
      2. “GenerateQrCodeUri” method in “Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs”
      3. TOTP enrollment is performed by the user after regular sign-in using the mobile phone to scan the QR code on the page

  • Jason says:

    Hi,
    According to this site below. the 2FA code is valid for six minutes, could you please how to custom this default value.
    https://docs.microsoft.com/en-us/aspnet/identity/overview/features-api/two-factor-authentication-using-sms-and-email-with-aspnet-identity

    Thanks & Best Regards.
    /Jason

    • deblokt says:

      Hi Jason. I don’t use SMS or email in my tutorials. I suggest switching to TOTP or even better FIDO2 for a secure second factor.

  • Edwin Kwok says:

    Very Great demo, works like a charm

  • Marcos Arruda says:

    Excellent work. But could you please update the project on GitHub to be compatible with IdentityServer4 version 4.0.x? There were a lot of changes from version 3.1.x.

    • deblokt says:

      We currently don’t have available resources to update the tutorials. If you would like to contribute by creating the tutorials for the latest ID4 version please get in touch and we can publish your post or add a link to your blog post. Thank you.

  • David says:

    I ivan I followed your tutorial but is2faTokenValid is always false I have setup the requirments in startup.cs but I dont understand why its always being false

    • deblokt says:

      I don’t have enough information to provide any useful feedback. Please clone the repo and try again.

Comments are closed.