(C# ASP.NET Core) Account Verification in an Identity based Authentication Project

Recall from the previous tutorial that in our program.cs file we have set SignIn.RequireConfirmedAccount = true. This prevents immediate login to a newly registered user. The user is compulsorily required to verify his account through an email or SMS sent to his phone. For this, a link containing a specially generated confirmation token is sent to him. When the user clicks this link then this token is verified on the server side, and the user is informed of the confirmation status.
(Rev. 19-Mar-2024)

Categories | About |     |  

Parveen,

Table of Contents (top down ↓)

Recap of the Previous Tutorial

Here is a quick recap of what we have done in the previous tutorial.

In the last tutorial we registered a user account - but we didn't perform the confirmation. Just for curiosity, let's try to login.

Open the solution explorer and run the project. Click the link to attempt open the restricted page. We are redirected to the login page. Enter the email and the password to try a login.

The login fails, as expected, because the account is not yet confirmed. (see the linked video for screenshots)

Video Explanation (see it happen!)

Please watch the following youtube video:

The plan we shall follow now

Let us have a look at the plan that we shall follow in this tutorial.

Our current objective is to add code for account confirmation through email or sms - but we won't be able to send any email or any sms because it is just a tutorial.

So, we shall create a page called EMailTestPage, and redirect a newly registered user to that test page. This page shall contain the link that has to be sent to a user in a real world application.

The user will click that link and he will be taken to a ConfirmAccount page for confirmation of his account.

Step 1 - Complete the Registration function

We start by completing the registration function from the point at which we left in the previous tutorial.

Remove the code where we displayed a success message.

// see OnPostAsync() in the previous tutorial 

// code not shown 
// . 
// . 
// . 

// code not shown 
if (!result.Succeeded)
{


  // code not shown 
  // . 
  // . 
  // . 

  // code not shown 
}

else 
{

  // REMOVE THIS PART 
  String message = "SUCCESS: Registration done! In the next tutorial " +
  "we add code for email confirmation because in our program.cs " +
  "file we have configured options.SignIn.RequireConfirmedAccount = true";

  // just using this for temporarily displaying 
  // the above message 
  ModelState.AddModelError(string.Empty, message);

}


// code not shown 
// . 
// . 
// . 

// code not shown 

Write the else part as shown -

// see OnPostAsync() in the previous tutorial 

// code not shown 
// . 
// . 
// . 

// code not shown 
if (!result.Succeeded)
{


  // code not shown 
  // . 
  // . 
  // . 

  // code not shown 
}

else 
{

  var code = await _um.GenerateEmailConfirmationTokenAsync(user);

  // encode as base64 
  code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

  return RedirectToPage("EMailTestPage",
    new { userId = user.Id, code = code });

}


// code not shown 
// . 
// . 
// . 

// code not shown 

I have attached the source code in the downloads for this course.

The completed source course as visual studio 2022 project is given in your downloads.

To make the things very clear, let me discuss the completed RegisterModel class.

// Register.cshtml.cs file 
// see previous tutorial for 
// detailed explanation of this file 
using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.RazorPages;

using Microsoft.AspNetCore.WebUtilities;

using System.ComponentModel.DataAnnotations;

using System.Text;

namespace MyRazorApp.Areas.Auth.Pages
{

  [AllowAnonymous]
  public class RegisterModel : PageModel
  {

    // UserID 
    [BindProperty]
    [Required]
    [DataType(DataType.EmailAddress)]
    public String? UserID { get; set; }

    // phone (optional) 
    [BindProperty]
    [Required]
    [DataType(DataType.PhoneNumber)]
    [RegularExpression(@"^\d{10}$", ErrorMessage = "Exactly 10 digits")]
    public String? UserPhone { get; set; }

    // password 
    [BindProperty]
    [Required]
    [DataType(DataType.Password)]
    public String? UserPassword { get; set; }

    // confirm password 
    [BindProperty]
    [Required]
    [Compare("UserPassword", ErrorMessage = "Passwords do not match")]
    [DataType(DataType.Password)]
    public string? ConfirmPassword { get; set; }

    // user manager 
    private readonly UserManager<IdentityUser> _um;

    public RegisterModel(UserManager<IdentityUser> um)
    {

      _um = um;

    }

    public async Task<IActionResult> OnPostAsync()
    {

      if (!ModelState.IsValid)
      {

        return Page();

      }

      var user = new IdentityUser()
      {

        UserName = UserID,
        Email = UserID,
        PhoneNumber = UserPhone
      };

      var result = await _um.CreateAsync(user, UserPassword);

      if (!result.Succeeded)
      {

        foreach (var error in result.Errors)
        {

          ModelState.AddModelError(string.Empty, error.Description);

        }

      }

      else 
      {

        // added now 
        var code = await _um.GenerateEmailConfirmationTokenAsync(user);

        // encode as base64 
        code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

        return RedirectToPage("EMailTestPage",
           new { userId = user.Id, code = code });

      }

      return Page();

    }

  }

}

We have the namespace directives at the top.

After that we have the bind properties for email, phone. These properties are connected to the registration form.

And, then we have the bind properties for password and confirm password.

Constructor based dependency injection has been used to obtain the UserManager service. We have cached it as a read-only member. This service is required for registration and for generating the account confirmation tokens.

The OnPostAsync method is called when a user posts and submits his registration form.

An IdentityUser is created on the basis of the data submited by the user.

Registration is done by the CreateAsync method of the UserManager service.

If the function fails, the errors are added to the model state, and the user is informed accordingly.

If the registration succeeds, then the GenerateEmailConfirmationTokenAsync method of the UserManager service is used to generate the email confirmation token. Next, this token is encoded as a base64 string. The user is then redircted to the email test page.

Step 2 - Add the EMail Test Page

Now we shall add the email test page.

Open the solution explorer and right click the Pages folder in the root directory, and add a page called EMailTestPage.

Double click to open the page. Let us examine the markup code for this page.

// Pages -> EMailTestPage.cshtml  
@page

@functions {

  public String? Link { get; set; }

  public void OnGet(String userID, String code)
  {
    Link = Url.Page(
                "/ConfirmAccount",
                null,
                new { area = "Auth", userID = userID, code = code },
                Request.Scheme);
  }
}

<h1>Test EMail</h1>

<p>
  NOTE: in real application this link is sent in an email to the user. 
  He has to click the link for email verification.
</p>

<h2>Please click to confirm your email</h2>

<a href="@(Link)">Click to Confirm</a>


  

This page simulates the email that has to be sent to the newly registered user. Basically, it has to show a link.

We have added a property called Link to hold the path to the ConfirmAccount page that we shall soon be adding (in Step 3 next).

The OnGet function receives userId and code as its parameters from the registration page. The link is created with the built-in Url.Page method. The link has been created with the same userId and code parameters.

Finally, an anchor link is added so that the user can click and reach the ConfirmAccount page.

Step 3 - Add the ConfirmAccount Page

Lastly, we shall add the confirm account page.

Open the solution explorer and locate the auth area. Right click the Pages folder and add a page called ConfirmAccount.

Let us examine the markup code for this page.

// Areas -> Auth -> Pages -> ConfirmAccount.cshtml  
// line by line explanation in the linked video 
// or you can read the text that follows this code 
@page

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using System.Text;

@inject UserManager<IdentityUser> _um;

@functions {

  public String? Message { get; set; }

  public async Task<IActionResult> OnGetAsync(String userID, String code)
  {
    String _code =
      Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));

    var user = await _um.FindByIdAsync(userID);

    var result = await _um.ConfirmEmailAsync(user, _code);

    if (result.Succeeded)
    {
      Message = "SUCCESS: Verified successfully!";
    }
    else
    {
      Message = "EMail verification failed.";
    }

    return Page();

  }
}

<h1>Confirm Account</h1>

<div>

  <span style="color:red">@Message</span>

  <p>

    @if (true == Message?.StartsWith("SUCCESS"))
    {
      <a asp-page="Login">Click to Login!</a>
    }

  </p>

</div>

  

First we have the various namespace directives. You can always come back to resolve any compilation errors for this file.

The @inject directive has been used to obtain the UserManager service. This service is required for confirming the user account.

The Message property will be used to display any messages to the user.

The OnGet method receives userId and code from the link that the user clicked to reach this page.

First the code is decoded, and then userId is used to find the IdentityUser.

The ConfirmEmailAsync method performs the whole operation of confirming the user. Notice that all database communication has been handled by the UserManager service.

If the user confirmation succeeds, a success message is set. The message is displayed later on the same page.

But if the confirmation fails, then the failure message is set on the Message property.

The Message property is displayed in red color on the same page. If the message is a success message, then a link to the login page is also shown.

Run the Project

Let's now run the project!

Open the solution explorer, and run the project to open the home page. Click on the link to restricted page to try open it.

We are redirected to the login page. Click on the register link to register a new user.

The registration page opens. Enter some data, and click the create button.

If the registration succeeds, we are taken to the test email page. Now click the Click to Confirm link (see the linked video for screenshots).

We are redirected to the confirmation page and shown a success message. We are also presented a link for Click to Login. Click the link to reach the login page.

Enter the credentials and click the login button.

If everything goes fine, we are on the home page with the message "You are logged in!"

Now we can click the link Click to View Restricted Page. We verify that we are successfully able to view the restricted page. Things are working as expected. We were able to register a user, and confirm his account successfully. Thanks!


This Blog Post/Article "(C# ASP.NET Core) Account Verification in an Identity based Authentication Project" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.