Step 1 - Add link to Enable 2-factor Authentication
Let us first add an anchor link that takes a user to the enable authentication page.
Open the solution explorer and locate the _LoginPartial partial page. Double click to open the page.
// Pages -> Shared -> _LoginPartial.cshtml @using Microsoft.AspNetCore.Identity @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject SignInManager<IdentityUser> _sm @if (_sm.IsSignedIn(User)) { <div> <span>Welcome @User.Identity?.Name</span> | <a asp-area="auth" asp-page="Logout">Logout</a> | <a asp-area="auth" asp-page="Mfa/EnableAuthenticator"> Enable 2-Factor Auth </a> </div> } else { <div> <a asp-area="auth" asp-page="Register">Register</a> | <a asp-area="auth" asp-page="Login"> Login </a> </div> }
We have already completed and discussed this page - so we shall directly come to the if
block that displays the user name of the logged user, and the links for logout. Now we have added a link to EnableAuthenticator page.
Video Explanation (see it happen!)
Please watch the following youtube video:
Step 2 - Add the EnableAuthenticator Page
Next let us add the EnableAuthenticator page. Open the solution explorer and locate the pages folder of the Auth area.
Add a folder called Mfa to the pages folder so that we can keep the related pages in a common folder. Right click the Mfa folder to add a page called EnableAuthenticator.
This page has to show a token code that the user will be required to enter in his authenticator app. The same page has to show a form where the user has to enter the code that he will receive from the authenticator app. Let us have a look at the markup for this page.
// Areas -> Auth -> Pages -> Mfa -> EnableAuthenticator.cshtml @page @model MyRazorApp.Areas.Auth.Pages.Mfa.EnableAuthenticatorModel @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <h1> Enable 2-factor Authentication </h1> <p> Enter the following code in your authenticator App: </p> <h2 style="color:blue"> @Model.SharedKey </h2> <p> Your app will give a code. <br/> Enter that code below to enable 2-factor authentication. </p> <div style="color:red;" asp-validation-summary="All"></div> <form method="post"> <table> <tr> <td> <label asp-for="Code"></label>: </td> <td> <input asp-for="Code" /> </td> <td> <span asp-validation-for="Code"></span> </td> </tr> <tr> <td colspan="2" style="text-align:right"> <input type="submit" value="Enable 2-factor Auth!" /> </td> </tr> </table> </form>
First we have the directives for page, model and addTagHelper. Then we display the token code in a prominent h2 tag. The property SharedKey will be set in the backing class for this page.
After that we have a validation summary for the various model errors. Then we have used an html table tag to write the form for the code from the validator app. The first tr tag contains the input textbox, and the second contains the submit button.
Step 3 - Backing class for the EnableAuthenticator Page
We can how have a look at the backing class for this page. Open the solution explorer and locate the EnableAuthenticator.cshtml.cs file. Double click to open this file.
// Areas -> Auth -> Pages -> Mfa -> EnableAuthenticator.cshtml.cs using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.ComponentModel.DataAnnotations; using System.Text; namespace MyRazorApp.Areas.Auth.Pages.Mfa { public class EnableAuthenticatorModel : PageModel { private readonly UserManager<IdentityUser> _um; public EnableAuthenticatorModel(UserManager<IdentityUser> um) { _um = um; } // key shown to the user public String? SharedKey { get; set; } // OnGet method where we set the above SharedKey public async Task<IActionResult> OnGetAsync() { IdentityUser user = await _um.GetUserAsync(User); if (null == user) { return NotFound("problem with the user account"); } var authKey = await _um.GetAuthenticatorKeyAsync(user); if (string.IsNullOrEmpty(authKey)) { await _um.ResetAuthenticatorKeyAsync(user); authKey = await _um.GetAuthenticatorKeyAsync(user); } StringBuilder sbKey = new(); // add space after 4 chars for visibility for (int i = 0; i < authKey.Length; i++) { if (0 == (i % 4)) { sbKey.Append(' '); } sbKey.Append(authKey[i]); } SharedKey = sbKey.ToString(); ; return Page(); } [BindProperty] [Required] public string? Code { get; set; } // message to be shown on redirect // to TwoFactorAuth page [TempData] public string? Message { get; set; } // on post method when the user // posts the token received from the // authenticator app public async Task<IActionResult> OnPostAsync() { if (ModelState.IsValid) { String token = Code!.Replace(" ", String.Empty).Replace("-", String.Empty); var user = await _um.GetUserAsync(User); var istokenValid = await _um.VerifyTwoFactorTokenAsync( user, _um.Options.Tokens.AuthenticatorTokenProvider, token); if (istokenValid) { var res = await _um.SetTwoFactorEnabledAsync(user, true); if (res.Succeeded) { Message = "Authenticator App is verified!"; return RedirectToPage("./TwoFactorAuth"); } } ModelState.AddModelError(String.Empty, "Invalid code."); } return await OnGetAsync(); } } }
First we have the namespaces. Then we have the EnableAuthenticatorModel class. Constructor based dependency injection has been used to obtain the UserManager service. The instance has been cached as a readonly member.
Then we have a property for the shared key. This property is set in the OnGet method, and contains the authenticator token. This token is displayed on the page so that the user can read it enter it into his authenticator app.
UserManager
service is used in the OnGet method to obtain the IdentityUser
. Then the IdentityUser is used to obtain the authenticator key. This key is a continuous series of characters, which makes it difficult to read. So we use a for loop to insert spaces after every four characters, which makes it well formatted for reading. Finally, the SharedKey
property is set.
Next we have a bind property for Code. The OnPost method is called when the user posts the form. Any spaces or hyphens in the code are removed to obtain a continuous token. The user manager service is used to verify this token.
If the token is valid, two factor authentication is enabled and the user is redirected to a page where more options related to 2-factor authentication can be presented to him.
Next let us add the page TwoFactorAuth to present more options to the user.
Step 4 - Adding the TwoFactorAuth Page
Open the solution explorer and rightclick the Mfa folder to add the TwoFactorAuth page. Double click to open it!
// Areas -> Auth -> Pages -> Mfa -> TwoFactorAuth.cshtml @page @model MyRazorApp.Areas.Auth.Pages.Mfa.TwoFactorAuthModel <h1>Two Factor Authentication</h1> @if (TempData.ContainsKey("Message")) { <p style="color:red"> <span>@TempData["Message"]</span> </p> }
We shall complete this page in the next tutorial. But now we have added it so that we can view the status of success.
Run the Project
Run the project to open the home page. Click the login button and login into the website. We verify that the Enable 2-Factor Auth link appears. Click the link. Now we see a code for the authenticator app.
Open the authenticator app - and enter this code in the box shown there. You will be shown a code that is valid for 30 seconds. Quickly read that code and enter it into the form and click the Enable 2-factor Auth button. If everything goes fine, you will see a success message.
In the next tutorial we shall complete the TwoFactorAuth page. Thanks!
This Blog Post/Article "(C# ASP.NET Core) Enabling 2-factor Authentication in Identity" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.