'C# - Two-Factor Authentication (without ASP.NET Core Identity)
Is it possible to implement a two-factor authentication without use ASP.NET Core Identity?
I have my own login table and everything works fine with login + password + jwt, but my customer wants to put a extra security using a two-factor authentication via SMS or E-mail.
I thought about Authy from Twilio but I don't understand if it is possible to do without ASP.NET Core Identity.
Solution 1:[1]
Twilio's Two Factor Authentication (2FA) services do not depend on the ASP.NET Core Identity framework, so you are not required to use it in combination with Identity, tho you can. Before providing more details, I do want to get on the same page on Twilio's 2FA products to avoid confusion.
Twilio has a couple of 2FA products as explained in this comparison page:
- Twilio Authy application: A free consumer application to generate Time-based One-Time Passwords (TOTP).
- Twilio Authy API: An API for developers to integrate 2FA into your applications. The Twilio Verify API superseeds the Twilio Authy API.
- Twilio Verify API: An API for developers to integrate 2FA into your applications. Verify API has an improved developer experience and supports more channels than the Authy API.
Twilio recommends using the Verify API for new development, so I'll be using the Verify API for this answer, however, let me know if you need a Authy API sample for some reason.
You can add the Twilio Verify API to any application, for example, here's a console application to verify a phone number:
string accountSid = configuration["Twilio:AccountSid"];
string authToken = configuration["Twilio:AuthToken"];
string verificationServiceSid = configuration["Twilio:VerifyServiceSid"];
TwilioClient.Init(accountSid, authToken);
Console.Write("What is your phone number? ");
string phoneNumber = Console.ReadLine();
var verification = await VerificationResource.CreateAsync(
to: phoneNumber,
channel: "sms",
pathServiceSid: verificationServiceSid
);
Console.WriteLine("Verification status: {0}", verification.Status);
Console.Write("Enter the code texted to your phone: ");
string code = Console.ReadLine();
var verificationCheck = await VerificationCheckResource.CreateAsync(
verificationSid: verification.Sid,
code: code,
pathServiceSid: verificationServiceSid
);
Console.WriteLine("Verification status: {0}", verificationCheck.Status);
The configuration
comes from the ConfigurationBuilder
which you can learn more about here.
You can find the Twilio:AccountSid
and Twilio:AuthToken
in the Twilio Console.
To use the Verify API, you need to create a Verify Service, which you can also do in the Twilio Console (or using the API, SDK, or CLI).
The Verify Service will have an String Identifier (SID) which I'm storing in Twilio:VerifyServiceSid
.
Once the program has the necessary Twilio configuration, it'll initialize the TwilioClient
, then it'll ask the user for a phone number to verify, then it uses the phone number to create a VerificationResource
.
When this VerificationResource
is created, Twilio will send a code to the phone number.
The program will ask for that code and then use the code to create the VerificationCheckResource
.
The status of the created VerificationCheckResource
will indicate whether the code was correct.
Status
can have three different string values:
- pending: This is the default status, but also the status when a code was provided, but not the correct code.
- approved: The provided code was correct, the phone number is verified.
- cancelled: The verification has been cancelled, you can create a new verification to start over.
This example uses SMS as a channel, but the flow is more or less the same for other channels. The above sample merged the samples from the Twilio Docs on Verify for C#.
You can now apply the same logic into your ASP.NET Core applications as you see fit. Here's an example of an ASP.NET Core MVC controller with some actions and views to verify a phone number via SMS.
HomeController.cs:
using Microsoft.AspNetCore.Mvc;
using Twilio.Rest.Verify.V2.Service;
using TwilioVerifyAspNet.Models;
public class HomeController : Controller
{
private readonly string verifyServiceSid;
public HomeController(IConfiguration configuration)
{
verifyServiceSid = configuration["Twilio:VerifyServiceSid"];
}
[HttpGet]
public IActionResult Index() => View();
[HttpGet]
public IActionResult RequestTwoFactorToken() => View();
[HttpPost]
public async Task<IActionResult> RequestTwoFactorToken(RequestTwoFactorViewModel requestTwoFactorViewModel)
{
if (!ModelState.IsValid)
{
return View(requestTwoFactorViewModel);
}
var verificationResource = await VerificationResource.CreateAsync(
to: requestTwoFactorViewModel.PhoneNumber,
channel: "sms",
pathServiceSid: verifyServiceSid
);
var verifyTwoFactorViewModel = new VerifyTwoFactorViewModel
{
VerificationSid = verificationResource.Sid,
Code = null
};
return View("VerifyTwoFactorToken", verifyTwoFactorViewModel);
}
[HttpPost]
public async Task<IActionResult> VerifyTwoFactorToken(VerifyTwoFactorViewModel verifyTwoFactorViewModel)
{
if (!ModelState.IsValid)
{
return View(verifyTwoFactorViewModel);
}
var verificationCheck = await VerificationCheckResource.CreateAsync(
verificationSid: verifyTwoFactorViewModel.VerificationSid,
code: verifyTwoFactorViewModel.Code,
pathServiceSid: verifyServiceSid
);
switch (verificationCheck.Status)
{
case "pending":
verifyTwoFactorViewModel.Status = "pending";
return View("VerifyTwoFactorToken", verifyTwoFactorViewModel);
case "canceled":
verifyTwoFactorViewModel.Status = "canceled";
return View("VerifyTwoFactorToken", verifyTwoFactorViewModel);
case "approved":
return View("Success");
}
return View("VerifyTwoFactorToken", verifyTwoFactorViewModel);
}
}
RequestTwoFactorViewModel.cs:
using System.ComponentModel.DataAnnotations;
public class RequestTwoFactorViewModel
{
[Required]
public string PhoneNumber { get; set; }
}
VerifyTwoFactorViewModel.cs:
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class VerifyTwoFactorViewModel
{
[Required]
public string? VerificationSid { get; set; }
[Required]
public string? Code { get; set; }
// prevent user from setting the status via form
[BindNever]
public string? Status { get; set; }
}
RequestTwoFactorToken.cshtml:
@model RequestTwoFactorViewModel
<form asp-controller="Home" asp-action="RequestTwoFactorToken">
<label for="phone-number">Phone Number</label> <br>
<input id="phone-number" asp-for="PhoneNumber"/> <br>
<button type="submit">Submit</button>
</form>
VerifyTwoFactorToken.cshtml:
@model VerifyTwoFactorViewModel
@if (Model.Status == null || Model.Status == "pending")
{
<form asp-controller="Home" asp-action="VerifyTwoFactorToken">
<input type="hidden" asp-for="VerificationSid" />
<label for="code">Code</label> <br>
<input id="code" asp-for="Code"/> <br>
<button type="submit">Submit</button>
</form>
}
else if (Model.Status == "cancelled")
{
<text>Your 2FA verification has been cancelled</text>
}
You do have to make sure you initialize the TwilioClient
at startup of your application.
The RequestTwoFactorToken
HTTP GET action will render a form to ask for the phone number.
When the form is submitted, the RequestTwoFactorToken
HTTP POST action will create the VerifyResource
, which is used to create the VerifyTwoFactorViewModel
to render the VerifyTwoFactorToken.cshtml
view. The VerifyTwoFactorToken.cshtml
view asks the user for the SMS code.
When this form is submitted, the VerifyTwoFactorToken
action will create a VerificationCheckResource
. The resulting status is evaluated to see whether the code was correct or not.
This is a sample and by no means production ready, but it contains all the pieces you need to implement this into your own application. Hopefully this helps, I can't wait to see what you build!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Swimburger |