Verifying Phone Number Ownership in ASP.NET Core Identity with Twilio Verify v2 and Razor Pages

May 31, 2019
Written by
Andrew Lock
Contributor
Opinions expressed by Twilio contributors are their own

Copy of Product Template - Verify.png

ASP.NET Core Identity is a membership system that adds user sign in and user management functionality to ASP.NET Core apps. It includes many features out of the box and has basic support for storing a phone number for a user. By default, ASP.NET Core Identity doesn't try to verify ownership of phone numbers, but you can add that functionality yourself by integrating Twilio’s identity verification features into your application.

In this post you'll learn how you can prove ownership of a phone number provided by a user using Twilio Verify in an ASP.NET Core application using Razor Pages. This involves sending a code in an SMS message to the provided phone number. The user enters the code received and Twilio confirms whether it is correct. If so, you can be confident the user has control of the provided phone number.

You typically only confirm phone number ownership once for a user. This is in contrast to two-factor authentication (2FA) where you might send an SMS code to the user every time they login. Twilio has a separate Authy API for performing 2FA checks at sign in, but it won't be covered in this post.

Note that this post uses version 2.x of the Twilio Verify API. A previous post on the Twilio blog shows how to use version 1 of the API .

Prerequisites

To follow along with this post you'll need:

You can find the complete code for this post on GitHub.

If you would like to see a full integration of Twilio APIs in a .NET Core application then checkout this free 5-part video series. It's separate from this blog post tutorial but will give you a full run down of many APIs at once.

Creating the case study project

Using Visual Studio 2017+ or the .NET CLI, create a new solution and project with the following characteristics:

  • Type: ASP.NET Core 2.2 Web Application (not MVC) with Visual C#
  • Name: SendVerificationSmsV2Demo
  • Solution directory / uncheck Place solution and project in the same directory
  • Git repository
  • https
  • Authentication: Individual user accounts, Store user accounts in-app

ASP.NET Core Identity uses Entity Framework Core to store the users in the database, so be sure to run the database migrations in the project folder after building your app. Execute one of the following command line instructions to build the database:

.NET CLI

dotnet ef database update

Package Manager Console

update-database

The Twilio C# SDK and the Twilio Verify API

The Twilio API is a typical REST API, but to make it easier to work with Twilio provides helper SDK libraries in a variety of languages. Previous posts have shown how to use the C# SDK to validate phone numbers, and how to customize it to work with the ASP.NET Core dependency injection container.

Version 1.x of the Twilio Verify API was not supported by the C# SDK, so you had to make "raw" HTTP requests with an HttpClient. Luckily the SDK has been updated to work with version 2 of the Verify API, which drastically simplifies interacting with the API. Version 2 of the API also allows working with E.164 formatted numbers.

Initializing the Twilio API

Install the Twilio C# SDK by installing the Twilio NuGet package (version 5.29.1 or later) using the NuGet Package Manager, Package Manager Console CLI, or by editing the SendVerificationSmsV2Demo.csproj file. After using any of these methods the <ItemGroup> section of the project file should look like this (version numbers may be higher):

<ItemGroup>
 <PackageReference Include="Microsoft.AspNetCore.App"/>
 <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
 <PackageReference Include="Twilio" Version="5.29.1" />
</ItemGroup>

To call the Twilio API you'll need your Twilio Account SID and Auth Token (found in the Twilio Dashboard). When developing locally these should be stored using the Secrets Manager or as environment variables so they don't get accidentally committed to your source code repository. You can read about how and why to use the Secrets Manager in this post on the Twilio Blog. You'll also need the Service SID for your Twilio Verify Service (found under Settings for the Verify Service you created, in the Verify section of the Twilio Dashboard). Your resulting Secrets.json should look something like this:

{
 "Twilio": {
   "AccountSID": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
   "AuthToken": "your_auth_token",
   "VerificationServiceSID": "VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 }
}

The Twilio helper libraries use a singleton instance of the Twilio client, which means you only need to set it up once in your app. The best place to configure things like this are in the Startup.cs file. Add using Twilio; at the top of Startup.cs, and add the following at the end of ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
   // existing configuration

   var accountSid = Configuration["Twilio:AccountSID"];
   var authToken = Configuration["Twilio:AuthToken"];
   TwilioClient.Init(accountSid, authToken);
}

This sets up the static Twilio client using your credentials using the ASP.NET Core configuration system. If you need to customize the requests made to Twilio (by using a proxy server, for example), or want to make use of HttpClientFactory features introduced in ASP.NET Core 2.1, see a previous post on the Twilio blog for an alternative approach.

You also need to make the Verify Service ID accessible in the app, so create a strongly-typed Options object and bind the settings to it. Create the file TwilioVerifySettings.cs in the project directory and add the following code:

public class TwilioVerifySettings
{
   public string VerificationServiceSID { get; set; }
}

You can bind this class to the configuration object so it's accessible from everywhere via the dependency injection container. Add the following line to the end of the Startup.ConfigureServices method:

services.Configure<TwilioVerifySettings>(Configuration.GetSection("Twilio"));

With the Twilio SDK configuration complete, you can start adding the phone verification functionality to your ASP.NET Core Identity application.

Adding the required scaffolding files

In this post you're going to be adding some additional pages to the Identity area. Typically when you're adding or editing Identity pages in ASP.NET Core you should use the built-in scaffolding tools to generate the pages, as shown in this post. If you've already done that, you can skip this section.

Rather than adding all the Identity scaffolding, all you need for this post is a single file. Create the file _ViewImports.cshtml in the Areas/Identity/Pages folder and add the following code:

@using Microsoft.AspNetCore.Identity
@using SendVerificationSmsV2Demo.Areas.Identity
@namespace SendVerificationSmsV2Demo.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

This adds all the namespaces and tag helpers required by your Razor Pages. It will also light up the IntelliSense in Visual Studio. If you've already scaffolded Identity pages you'll already have this file!

Sending a verification code to a phone number

The default ASP.NET Core Identity templates provide the functionality for storing a phone number for a user, but don't provide the capability to verify ownership of the number. In the post Validating Phone Numbers in ASP.NET Core Identity Razor Pages with Twilio Lookup you can learn how to validate a phone number by using the Twilio Lookup API.

As noted in the previous post, it’s a good idea to store the result formatted as an E.164 number. This post assume you've done that so the PhoneNumber property for an IdentityUser is the E.164 formatted phone number.

Create a new folder Account, under the Areas/Identity/Pages folder, and add a new Razor Page in the folder called VerifyPhone.cshtml. Replace the VerifyPhoneModel class in the code-behind file VerifyPhone.cshtml.cs with the following:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Twilio.Rest.Verify.V2.Service;

namespace SendVerificationSmsV2Demo.Areas.Identity.Pages.Account
{
   [Authorize]
   public class VerifyPhoneModel : PageModel
   {
       private readonly TwilioVerifySettings _settings;
       private readonly UserManager<IdentityUser> _userManager;

       public VerifyPhoneModel(IOptions<TwilioVerifySettings> settings, UserManager<IdentityUser> userManager)
       {
           _settings = settings.Value;
           _userManager = userManager;
       }

       public string PhoneNumber { get; set; }

       public async Task<IActionResult> OnGetAsync()
       {
           await LoadPhoneNumber();
           return Page();
       }

       public async Task<IActionResult> OnPostAsync()
       {
           await LoadPhoneNumber();

           try
           {
               var verification = await VerificationResource.CreateAsync(
                   to: PhoneNumber,
                   channel: "sms",
                   pathServiceSid: _settings.VerificationServiceSID
               );

               if (verification.Status == "pending")
               {
                   return RedirectToPage("ConfirmPhone");
               }

               ModelState.AddModelError("", $"There was an error sending the verification code: {verification.Status}");
           }
           catch (Exception)
           {
               ModelState.AddModelError("",
                   "There was an error sending the verification code, please check the phone number is correct and try again");
           }

           return Page();
       }

       private async Task LoadPhoneNumber()
       {
           var user = await _userManager.GetUserAsync(User);
           if (user == null)
           {
               throw new Exception($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
           }
           PhoneNumber = user.PhoneNumber;
       }
   }
}

The phone number for the current user is loaded in the OnGetAsync using the LoadPhoneNumber helper method, and is assigned to the PhoneNumber property for display in the UI. The OnPostAsync handler is where the verification process begins.

The phone number is loaded again at the start of the OnPostAsync method and used to send a verification message with the Twilio helper SDK. The VerificationResource.CreateAsync method sends a verification code to the provided number using the Twilio Verify API. When calling this method you also need to provide the Verify Service ID. You retrieve the value from configuration by injecting an IOptions<TwilioVerifySettings> into the page constructor using the Options pattern.

The response from the Twilio Verify API contains a Status field indicating the overall status of the verification process. If the message is sent successfully, the response will return "pending", indicating that a check is waiting to be performed. On success, the user is redirected to the ConfirmPhone page, which you'll create shortly. If the Verify API indicates the request failed, or if an exception is thrown, an error is added to the ModelState, and the page is re-displayed to the user.

The form itself consists of just a message and a submit button. Replace the contents of VerifyPhone.cshtml with the following Razor markup:

@page
@model VerifyPhoneModel
@{
   ViewData["Title"] = "Verify Phone number";
}

<h4>@ViewData["Title"]</h4>
<div class="row">
   <div class="col-md-8">
       <form method="post">
           <p>
               We will verify your phone number by sending a code to @Model.PhoneNumber.
           </p>
           <div asp-validation-summary="All" class="text-danger"></div>
           <button type="submit" class="btn btn-primary">Send verification code</button>
       </form>
   </div>
</div>

When rendered, the form looks like the following:

Verify phone number form

To test the form, sign in to your app, navigate to /Identity/Account/Manage and add your phone number to the account. Remember to use an E.164 formatted number that includes your country code, for example +14155552671, and remember to click Save so the number is written to the database. 

Next, navigate to /Identity/Account/VerifyPhone in your browser's address bar and click Send verification code. If your phone number is valid, you’ll receive an SMS similar to the message shown below. Note that you can customize this message, including the service name and code length: see the Verify API documentation for details.

Your Twilio Verify API demo verification code is: 293312

At this point, your app will crash, as it’s trying to redirect to a page that doesn’t exist yet. Now you’ll need to create that page where the user enters the code they receive.

Checking the verification code

The check verification code page contains a single text box where the user enters the code they receive. Create a new Razor Page in the Areas/Identity/Pages/Account folder called ConfirmPhone.cshtml. In the code-behind file, ConfirmPhone.cshtml.cs, replace the ConfirmPhoneModel class with the following code:

using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Twilio.Rest.Verify.V2.Service;

namespace SendVerificationSmsV2Demo.Areas.Identity.Pages.Account
{
   [Authorize]
   public class ConfirmPhoneModel : PageModel
   {
       private readonly TwilioVerifySettings _settings;
       private readonly UserManager<IdentityUser> _userManager;

       public ConfirmPhoneModel(UserManager<IdentityUser> userManager, IOptions<TwilioVerifySettings> settings)
       {
           _userManager = userManager;
           _settings = settings.Value;
       }

       public string PhoneNumber { get; set; }

       [BindProperty, Required, Display(Name = "Code")]
       public string VerificationCode { get; set; }


       public async Task<IActionResult> OnGetAsync()
       {
           await LoadPhoneNumber();
           return Page();
       }

       public async Task<IActionResult> OnPostAsync()
       {
           await LoadPhoneNumber();
           if (!ModelState.IsValid)
           {
               return Page();
           }

           try
           {
               var verification = await VerificationCheckResource.CreateAsync(
                   to: PhoneNumber,
                   code: VerificationCode,
                   pathServiceSid: _settings.VerificationServiceSID
               );
               if (verification.Status == "approved")
               {
                   var identityUser = await _userManager.GetUserAsync(User);
                   identityUser.PhoneNumberConfirmed = true;
                   var updateResult = await _userManager.UpdateAsync(identityUser);

                   if (updateResult.Succeeded)
                   {
                       return RedirectToPage("ConfirmPhoneSuccess");
                   }
                   else
                   {
                       ModelState.AddModelError("", "There was an error confirming the verification code, please try again");
                   }
               }
               else
               {
                   ModelState.AddModelError("", $"There was an error confirming the verification code: {verification.Status}");
               }
           }
           catch (Exception)
           {
               ModelState.AddModelError("",
                   "There was an error confirming the code, please check the verification code is correct and try again");
           }

           return Page();
       }

       private async Task LoadPhoneNumber()
       {
           var user = await _userManager.GetUserAsync(User);
           if (user == null)
           {
               throw new Exception($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
           }
           PhoneNumber = user.PhoneNumber;
       }
   }
}

As before, the OnGetAsync handler loads the current user's phone number for display in the UI using the LoadPhoneNumber helper method. The phone number is loaded again in the OnPostAsync handler for calling the verification check API. You verify the user's code by calling VerificationCheckResource.CreateAsync, passing in the phone number, the provided verification code, and the Verify Service ID.

If the code is correct, the Verify API will return result.Status="approved". You can store the confirmation result on the IdentityUser object directly by setting the PhoneNumberConfirmed property and saving the changes.

If everything completes successfully, you redirect the user to a simple ConfirmPhoneSuccess page (that you'll create shortly). If there are any errors or exceptions, an error is added to the ModelState and the page is redisplayed.

Replace the contents of ConfirmPhone.cshtml with the Razor markup below:

@page
@model ConfirmPhoneModel
@{
   ViewData["Title"] = "Confirm Phone number";
}

<h4>@ViewData["Title"]</h4>
<div class="row">
   <div class="col-md-6">
       <form method="post">
           <p>
               We have sent a confirmation code to @Model.PhoneNumber
               Enter the code you receive to confirm your phone number.
           </p>
           <div asp-validation-summary="All" class="text-danger"></div>

           <div class="form-group">
               <label asp-for="VerificationCode"></label>
               <input asp-for="VerificationCode" class="form-control" type="number" />
               <span asp-validation-for="VerificationCode" class="text-danger"></span>
           </div>
           <button type="submit" class="btn btn-primary">Confirm</button>
       </form>
   </div>
</div>

@section Scripts {
   <partial name="_ValidationScriptsPartial" />
}

When rendered, this looks like the following:

Confirm phone number rendered page.

Once the user successfully confirms their phone number you can be confident they have access to it and you can use it in other parts of your application with confidence.

Showing a confirmation success page

To create a simple "congratulations" page for the user, create a new Razor Page in the Areas/Identity/Pages/Account folder called ConfirmPhoneSuccess.cshtml. You don't need to change the code-behind for this page, just add the following markup to ConfirmPhoneSuccess.cshtml:

@page
@model ConfirmPhoneSuccessModel
@{
   ViewData["Title"] = "Phone number confirmed";
}

<h1>@ViewData["Title"]</h1>
<div>
   <p>
       Thank you for confirming your phone number.
   </p>
   <a asp-page="/Index">Back to home</a>
</div>

After entering a correct verification code, users will be redirected to this page. From here, they can return to the home page.

Phone number confirmed screen

Trying out the Twilio Verify functionality

Try out what you’ve just built by running the app. Follow these steps to validate a user’s ownership of a phone number with Verify:

Navigate to /Identity/Account/Manage in your browser. Because this page is protected by ASP.NET Core authorization, you’ll be redirected to the account sign in page.

Register as a new user. You will be redirected to the account management route where you can enter your phone number, and click Save. Next, navigate to the  /Identity/Account/VerifyPhone route and you'll see the rendered VerifyPhone.cshtml Razor page, indicating that a verification code will be sent to the number you just entered.

Click Send verification code and you will be routed to /Identity/Account/ConfirmPhone. In a matter of moments you should receive an SMS message with a verification code. Note that the message reflects the name of the service you created in Twilio Verify.

At this point you can go to the Verify console at https://www.twilio.com/console/verify/services, select your Verify service, and view the logs. You should see a log with a status of "Pending" next to your phone number.

Enter the numeric code from the SMS message in the Code box on the Confirm phone number page and click Confirm. (Validation codes expire, so you need to do this within 10 minutes of receiving the code.)

If everything worked correctly you should be redirected to the /Identity/Account/ConfirmPhoneSuccess page. If you refresh the logs for your Verify service in the Twilio Console you should see the successful validation reflected in the "status" column.

Good work! You've successfully integrated Twilio Verify with ASP.NET Core 2.2 Identity.

Possible improvements

This post showed the basic approach for using version 2 of the Verify API with ASP.NET Core Identity, but there are many improvements you could make:

  • Include a link the VerifyPhone page. Currently you have to navigate manually to /Identity/Account/VerifyPhone, but in practice you would want to add a link to it somewhere in your app.
  • Show the verification status of the phone number in the app. By default, ASP.NET Core Identity doesn't display the IdentityUser.PhoneNumberConfirmed property anywhere in the app.
  • Only verify unconfirmed numbers. Related to the previous improvement, you probably only want to verify phone numbers once, so you should check for PhoneNumberConfirmed=true in the VerifyPhone page, as well as hide any verification links.
  • Allow re-sending the code. In some cases, users might find the verification code doesn't arrive. For a smoother user experience, you could add functionality to allow re-sending a confirmation code to the ConfirmPhone page.

Summary

In this post you saw how to use version 2 of the Twilio Verify API to confirm phone number ownership in an ASP.NET Core Identity application. You learned how to use the Twilio helper SDK to create a verification and a verification check, and how to update the ASP.NET Core Identity user once the phone number is confirmed.

You can find the complete sample code for this post on GitHub.

Andrew Lock is a Microsoft MVP and author of ASP.NET Core in Action by Manning. He can be reached on Twitter at @andrewlocknet, or via his blog at https://andrewlock.net.