Two-Factor Authentication ASP.NET MVC 4 Registration with Twilio SMS

Developer Long Le


This is a guest post by Long Le, a .NET Enterprise Architect and Solutions Architect at Computer Sciences Corporation. Originally posted on Long’s personal blog found here, Long is an avid blogger and a serious Modern Warfare 3 Gamer. Make sure to follow his code and gaming conversations on Twitter @LeLong37.

Some sites such as live.com, gmail.com will require a multi-step registration and/or forgot password workflows to validate you say you are. Having an opportunity working with the Twilio Cloud Communication Platform, exposed how easily this can be done with their Api’s.

So for this post, I wanted to illustrate the steps in getting your MVC 4 application wired up with multi-step registration process with SMS code verification leveraging Twilio. We will start from my last blog post with Seed Users and Roles with MVC 4, SimpleMembershipProvider, SimpleRoleProvider, EntityFramework 5 CodeFirst, and Custom User Properties.

Since we already gathered the user’s mobile number during registration, let’s go ahead and add a property/field “IsSmsVerified” and run EntityFramework’s migration command update-database -verbose (so we can see what commands are being issued to our database for the migration.

NuGet and install the Twilio.Mvc package.

Update our UserProfile entity with IsSmsVerified and SmsVerificationCode properties.

[sourcecode language="csharp" highlight="6,14,15,16"]

[Table("UserProfile")]
public class UserProfile
{
public UserProfile()
{
IsSmsVerified = false;
}

[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
public string Mobile { get; set; }
[DefaultValue(false)]
public bool IsSmsVerified { get; set; }
public string SmsVerificationCode { get; set; }
}

[/sourcecode]

Update our Seed method so that we are not inserting nulls for the provisioned users.

[sourcecode language="csharp" highlight="35"]

#region

using System.Data.Entity.Migrations;
using System.Linq;
using System.Web.Security;
using MVC4SimpleMembershipCodeFirstSeedingEF5.Models;
using WebMatrix.WebData;

#endregion

namespace MVC4SimpleMembershipCodeFirstSeedingEF5.Migrations
{
internal sealed class Configuration : DbMigrationsConfiguration<UsersContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}

protected override void Seed(UsersContext context)
{
WebSecurity.InitializeDatabaseConnection(
"DefaultConnection",
"UserProfile",
"UserId",
"UserName", autoCreateTables: true);

if (!Roles.RoleExists("Administrator"))
Roles.CreateRole("Administrator");

if (!WebSecurity.UserExists("lelong37"))
WebSecurity.CreateUserAndAccount(
"lelong37",
"password",
new {Mobile = "+19725000374", IsSmsVerified = false});

if (!Roles.GetRolesForUser("lelong37").Contains("Administrator"))
Roles.AddUsersToRoles(new[] {"lelong37"}, new[] {"Administrator"});
}
}
}

[/sourcecode]

Run: update-database -verbose from the Package Manager Console

Now the fun begins, let’s update our AccountController.

  • Update the Register(RegisterModel model) Action and introduce the second step registration process of entering an SMS verfication code that we send the user using Twilio’s REST Api Client.

    Note: We are just scratching the tip of the ice berg in terms of what the Twilio Cloud Communication offers, you can visit their docs site for more info.

  • Add SmsVerification() Action, so that the user can enter the SMS verification code.
  • Add SmsVerication(SmsVerificationModel smsVerificationModel) Action, so that we can validate the user, the user’s mobile number, and SMS verification code.
  • Add GenerateSimpleSmsVerificationCode() method, a simple static helper method to generate a six character SMS verification code.

[sourcecode language="csharp" highlight="19,20,21,25,26,27,29,30,31,32,33,34,35,36,"]

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
try
{
var smsVerificationCode =
GenerateSimpleSmsVerificationCode();

WebSecurity.CreateUserAndAccount(
model.UserName,
model.Password,
new
{
model.Mobile,
IsSmsVerified = false,
SmsVerificationCode = smsVerificationCode
},
false);

var twilioRestClient = new TwilioRestClient(
ConfigurationManager.AppSettings.Get("Twilio:AccoundSid"),
ConfigurationManager.AppSettings.Get("Twilio:AuthToken"));

twilioRestClient.SendSmsMessage(
"+19722001298",
model.Mobile,
string.Format(
"Your ASP.NET MVC 4 with Twilio " +
"registration verification code is: {0}",
smsVerificationCode)
);

Session["registrationModel"] = model;

return RedirectToAction("SmsVerification", "Account");
}
catch (MembershipCreateUserException e)
{
ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
}
}

// If we got this far, something failed, redisplay form
return View(model);
}

[AllowAnonymous]
public ActionResult SmsVerification()
{
return View(new SmsVerificationModel
{
Username =
((RegisterModel) Session["registrationModel"])
.UserName
});
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult SmsVerification(SmsVerificationModel smsVerificationModel)
{
if (ModelState.IsValid)
{
var userContext = new UsersContext();

var userProfile = userContext.UserProfiles
.Single(u => u.UserName == smsVerificationModel.Username);

var registerModel = ((RegisterModel) Session["registrationModel"]);

if (userProfile.SmsVerificationCode == smsVerificationModel.SmsVerificationCode)
{
WebSecurity.Login(userProfile.UserName, registerModel.Password);
return RedirectToAction("Index", "Home");
}
}

ModelState.AddModelError("", "The SMS verfication code was incorrect.");
return RedirectToAction("SmsVerification", "Account");
}

private static string GenerateSimpleSmsVerificationCode()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
return new string(
Enumerable.Repeat(chars, 6)
.Select(s => s[random.Next(s.Length)])
.ToArray());
}

[/sourcecode]

We could combine the two actions SmsVerication() and SmsVerication(SmsVerificationModel smsVerificationModel) into one, by checking the request verb for GET or Post, however for separation of concerns we will keep them “nice” and “separate”.

Let’s add some AppSettings entries to store our Twilio Rest Api credentials.

[sourcecode language="xml" highlight="7,8"]

<appSettings>
<add key="webpages:Version" value="2.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="PreserveLoginUrl" value="true" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="Twilio:AccoundSid" value="youtwilioaccountid" />
<add key="Twilio:AuthToken" value="yourtwilioauthtoken" />
</appSettings>

[/sourcecode]

Note: Your Twilio credentials for using their REST Api can be found on your dashboard after registering.

Create a SmsVerification ViewModel.

[sourcecode language="csharp"]

using System.ComponentModel.DataAnnotations;

using System.ComponentModel.DataAnnotations;

namespace MVC4SimpleMembershipCodeFirstSeedingEF5.Models
{
public class SmsVerificationModel
{
[Display(Name = "Username")]
public string Username { get; set; }

[Required]
[Display(Name = "SMS Verification Code")]
public string SmsVerificationCode { get; set; }
}
}

[/sourcecode]

Let’s create the SmsVerification View where a user can input the SMS verification code that we sent to the user bound to the ViewModel we just created.

[sourcecode language="csharp"]

@model MVC4SimpleMembershipCodeFirstSeedingEF5.Models.RegisterModel
@{
ViewBag.Title = "Register";
}

<hgroup class="title">
<h1>@ViewBag.Title.</h1>
<h2>Create a new account.</h2>
</hgroup>

@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.ValidationSummary()

<fieldset>
<legend>Registration Form</legend>
<ol>
<li>
@Html.LabelFor(m => m.UserName)
@Html.TextBoxFor(m => m.UserName)
</li>
<li>
@Html.LabelFor(m => m.Password)
@Html.PasswordFor(m => m.Password)
</li>
<li>
@Html.LabelFor(m => m.ConfirmPassword)
@Html.PasswordFor(m => m.ConfirmPassword)
</li>
<li>
@Html.LabelFor(m => m.Mobile)
@Html.TextBoxFor(m => m.Mobile)
</li>
</ol>
<input type="submit" value="Register" />
</fieldset>
}

@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

[/sourcecode]

Step 1 of the registration process, run the application and register.

For a quick sanity check let’s just make sure our SimpleMembershipProvider is persisting the extra properties we added earlier e.g. SmsVerificationCode, IsSmsVerified.

[sourcecode language="sql"]

SELECT TOP 1000 [UserId]
,[UserName]
,[Mobile]
,[IsSmsVerified]
,[SmsVerificationCode]
FROM [aspnet-MVC4SimpleMembershipCodeFirstSeedingEF5].[dbo].[UserProfile]

[/sourcecode]

Good, we can see here that Mobile, IsSmsVerified and SmsVerificationCode is being saved when we invoked the WebSecurity.CreateUserAndAccount method earlier from our Registration Action.

[sourcecode language="csharp"]

WebSecurity.CreateUserAndAccount(
model.UserName,
model.Password,
new
{
model.Mobile,
IsSmsVerified = false,
SmsVerificationCode = smsVerificationCode
},
false);

[/sourcecode]

Step 2, SMS notification to the user’s mobile number was received with the SMS verification code.

Step 3 of the registration process, input the SMS verification code in the SMSVerfication View.

You have now successfully completed the 3 step registration process and have been automatically logged into the site!

Now there are obviously TODO’s here, you can create an new authorize Attribute to verify that the IsSmsVerified property for the user is not false, clean up how we are storing the RegisterModel in session, additional bullet proofing the app in terms of security gaps, etc.. However the emphasis of this blog was multi-step registration to for increased validity of the user.

Last but not least, you can use the a similar implementation for things like forgot password or any other type of workflow that needs that extra degree of validation.

Happy Coding…! :)

Download sample application: https://skydrive.live.com/redir?resid=949A1C97C2A17906!2383