2FA is a must nowadays to increase the security within your application. It is seen in all kinds of apps: from the signup process to user action verification. The most common types of 2FA are email verification and phone verification. In this tutorial we'll show how to set up 2FA in your .NET application using ASP .NET Identity, the SendGrid C# Client for email auth and the Nexmo C# Client Library for SMS and Call-based auth.
Open Visual Studio and create a new ASP .NET MVC application. For this demo, we'll delete the Contact & About sections from the default generated website.
Add the Nexmo Client to your application via the NuGet Package Console.
PM> Install-Package Nexmo.Csharp.Client ​*-Version 6.3.3*
When doing this be sure to use the 6.3.3 version rather than the current 7.0.2 as there has been some difficulty with the namespaces for .NET 4.5.2
TODO: Can you install a specific version from the command line?
PM> Install-Package SendGrid via NuGet Package Manager
For the purpose of the demo we'll put the Nexmo and SendGrid credentials in the '' section of the'Web.config' file. If we were developing this application for distribution we may chose to enter these credentials in our Azure portal.
<add key="Nexmo.Url.Rest" value="https://rest.nexmo.com"/>
<add key="Nexmo.Url.Api" value="https://api.nexmo.com"/>
<add key="Nexmo.api_key" value="NEXMO_API_KEY"/>
<add key="Nexmo.api_secret" value="NEXMO_API_SECRET"/>
<add key="SMSAccountFrom" value="SMS_FROM_NUMBER"/>
<add key="mailAccount" value="SENDGRID_USERNAME"/>
<add key="mailPassword" value="SENDGRID_PASSWORD"/>
Inside the IdentityConfig.cs
file, add the SendGrid configuration in the SMSService
method. Then, plug in the Nexmo Client inside the SMSService
method of the IdentityConfig.cs
file. Add the using directives for the Nexmo and SendGrid namespaces.
public class EmailService : IIdentityMessageService
{
public async Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
await configSendGridasync(message);
}
private async Task configSendGridasync(IdentityMessage message)
{
var myMessage = new SendGridMessage();
myMessage.AddTo(message.Destination);
myMessage.From = new System.Net.Mail.MailAddress(
"demo@nexmo.com", "Nexmo2FADemo");
myMessage.Subject = message.Subject;
myMessage.Text = message.Body;
myMessage.Html = message.Body;
var credentials = new NetworkCredential(
ConfigurationManager.AppSettings["mailAccount"],
ConfigurationManager.AppSettings["mailPassword"]
);
// Create a Web transport for sending email.
var transportWeb = new Web(credentials);
// Send the email.
if (transportWeb != null)
{
await transportWeb.DeliverAsync(myMessage);
}
else
{
Trace.TraceError("Failed to create Web transport.");
await Task.FromResult(0);
}
}
}
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
var sms = SMS.Send(new SMS.SMSRequest
{
from = ConfigurationManager.AppSettings["SMSAccountFrom"],
to = message.Destination,
text = message.Body
});
return Task.FromResult(0);
}
}
Add the following method to your AccountController
which will be called on user registration to send a confirmation email to the provided email address.
private async Task<string> SendEmailConfirmationTokenAsync(string userID, string subject)
{
string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userID, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(userID, subject, "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");
return callbackUrl;
}
Inside the Register
method of the AccountController
, add a couple properties to newly created variable of the ApplicationUser type: TwoFactorEnabled
(true
), PhoneNumberConfirmed
(false
). Once the user is successfully created, store the user ID in a session state and redirect the user to the AddPhoneNumber
action method in the ManageController
.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, TwoFactorEnabled = true, PhoneNumberConfirmed = false};
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
Session["UserID"] = user.Id;
return RedirectToAction("AddPhoneNumber", "Manage");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
Add the [AllowAnonymous]
attribute to both the GET & POST AddPhoneNumber
action methods. This allows the user in the process of registering to access the phone number confirmation workflow. Make a query and check the DB if the phone number entered by the user is previously associated with an account. If not, redirect the user to the VerifyPhoneNumber
action method.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> AddPhoneNumber(AddPhoneNumberViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var db = new ApplicationDbContext();
if (db.Users.FirstOrDefault(u => u.PhoneNumber == model.Number) == null)
{
// Generate the token and send it
var code = await UserManager.GenerateChangePhoneNumberTokenAsync((string)Session["UserID"], model.Number);
if (UserManager.SmsService != null)
{
var message = new IdentityMessage
{
Destination = model.Number,
Body = "Your security code is: " + code
};
await UserManager.SmsService.SendAsync(message);
}
return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number });
}
else
{
ModelState.AddModelError("", "The provided phone number is associated with another account.");
return View();
}
}
Add the [AllowAnonymous]
attribute to the action method and delete everything in the method but the return statement that directs the verification flow,
[AllowAnonymous]
public async Task<ActionResult> VerifyPhoneNumber(string phoneNumber)
{
return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber });
}
Replace User.Identity.GetUserId()
with Session["UserID"]
in the method as shown below. If the user successfully enters the pin code, they are directed to the Index view of the Manage controller. The User's boolean property PhoneNumberConfirmed
is then set to true
.
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyPhoneNumber(VerifyPhoneNumberViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await UserManager.ChangePhoneNumberAsync((string)Session["UserID"], model.PhoneNumber, model.Code);
if (result.Succeeded)
{
var user = await UserManager.FindByIdAsync((string)Session["UserID"]);
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
return RedirectToAction("Index", new { Message = ManageMessageId.AddPhoneSuccess });
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "Failed to verify phone");
return View(model);
}
In the Login()
action method, check to see if the user has confirmed their email or not. If not, return an error message (ex: "You must have a confirmed email to login.") and redirect the user to the "Info" view. Also, call the SendEmailConfirmationTokenAsync()
method with the following parameters: user ID (Ex: user.Id) and the email subject (Ex: "Confirm your account.")
var user = await UserManager.FindByNameAsync(model.Email);
if (user != null)
{
if (!await UserManager.IsEmailConfirmedAsync(user.Id))
{
string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id, "Confirm your account");
ViewBag.title = "Check Email";
ViewBag.message = "You must have a confirmed email to login.";
return View("Info");
}
}
Inside the Account folder of the Views folder, create a new View named 'Info' that the user will be redirected to if their email has not been confirmed. The view should contain the following code:
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>
Inside the Account folder of the Views folder, edit the Login and VerifyCode views. In both files, delete the
With that, you have a web app using ASP .NET Identity that is 2 Factor Authentication enabled using Nexmo Verify and SendGrid Email as the different methods of verification.
2FA adds a layer of security to correctly identify users and further protect sensitive user information. Using Nexmo's C# Client Library and SendGrid's C# Client, you can add both email and phone verification for your 2FA solution with ease. Feel free to send me any thoughts/questions on Twitter @sidsharma_27 or email me at sidharth.sharma@nexmo.com!