How to Implement Phone verification, 2FA using ASP.NET Core

By FoxLearn 3/3/2025 8:21:54 AM   55
To implement phone verification and Two-Factor Authentication (2FA) using ASP.NET Core, we can extend ASP.NET Core Identity and use an SMS provider to send verification codes.

Set Up ASP.NET Core Identity

First, you need to set up ASP.NET Core Identity if you haven't already. You can follow the basic setup by creating a new project with ASP.NET Core Identity.

1. Create a new ASP.NET Core project with Identity: Use the ASP.NET Core Web Application template with Individual User Accounts.

2. Install Required Packages: Ensure you have the necessary packages in your project, including Microsoft.AspNetCore.Identity and Microsoft.EntityFrameworkCore.

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

To integrate the SMS provider into the ASP.NET Core Identity application, we use the PhoneNumberTokenProvider and an SMS verification service.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
builder.Services.AddTransient<IEmailSender, EmailSender>();

builder.Services.Configure<SmsOptions>(builder.Configuration.GetSection("SmsOptions"));

var authorization = Convert.ToBase64String(Encoding.ASCII.GetBytes(
    $"{builder.Configuration["SmsOptions:Username"]}:{builder.Configuration["SmsOptions:Password"]}"));

builder.Services.AddHttpClient(Consts.SMSeColl, client =>
{
    client.BaseAddress = new Uri($"{builder.Configuration["SmsOptions:Url"]}");
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorization);
});

builder.Services.AddScoped<SmsProvider>();

builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddTokenProvider<DataProtectorTokenProvider<ApplicationUser>>(TokenOptions.DefaultProvider)
    .AddTokenProvider<AuthenticatorTokenProvider<ApplicationUser>>(TokenOptions.DefaultAuthenticatorProvider)
    .AddTokenProvider<PhoneNumberTokenProvider<ApplicationUser>>(Consts.Phone)
    .AddTokenProvider<EmailTokenProvider<ApplicationUser>>(Consts.Email);

The ApplicationUser class needs new properties to support various authentication methods, such as enabling SMS-based 2FA or email-based verification.

public bool Phone2FAEnabled { get; set; }
public bool Email2FAEnabled { get; set; }
public bool AuthenticatorApp2FAEnabled { get; set; }
public bool Passkeys2FAEnabled { get; set; }

SMS Provider

The SmsProvider class integrates SMS services and handles phone number verification, enabling SMS-based 2FA, and forcing SMS-based 2FA. This implementation uses the eColl messaging service to send SMS.

public class SmsProvider
{
    private readonly HttpClient _httpClient;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SmsOptions _smsOptions;
    private readonly ILogger<SmsProvider> _logger;

    private const string Message = "message";

    public SmsProvider(IHttpClientFactory clientFactory,
        UserManager<ApplicationUser> userManager,
        IOptions<SmsOptions> smsOptions,
        ILogger<SmsProvider> logger)
    {
        _httpClient = clientFactory.CreateClient(Consts.SMSeColl);
        _userManager = userManager;
        _smsOptions = smsOptions.Value;
        _logger = logger;
    }

    public async Task<(bool Success, string? Error)> Send2FASmsAsync(ApplicationUser user, string phoneNumber)
    {
        var code = await _userManager.GenerateTwoFactorTokenAsync(user, Consts.Phone);
        var ecallMessage = new EcallMessage
        {
            To = phoneNumber,
            From = _smsOptions.Sender,
            Content = new EcallContent
            {
                Text = $"2FA code: {code}"
            }
        };

        var result = await _httpClient.PostAsJsonAsync(Message, ecallMessage);

        if (result.IsSuccessStatusCode)
        {
            var messageResult = await result.Content.ReadAsStringAsync();
            return (true, messageResult);
        }
        else
        {
            _logger.LogWarning("Error sending SMS 2FA, {ReasonPhrase}", result.ReasonPhrase);
            return (false, result.ReasonPhrase);
        }
    }

    public async Task<(bool Success, string? Error)> StartVerificationAsync(ApplicationUser user, string phoneNumber)
    {
        var token = await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber);
        var ecallMessage = new EcallMessage
        {
            To = phoneNumber,
            From = _smsOptions.Sender,
            Content = new EcallContent
            {
                Text = $"Verify code: {token}"
            }
        };

        var result = await _httpClient.PostAsJsonAsync(Message, ecallMessage);

        if (result.IsSuccessStatusCode)
        {
            var messageResult = await result.Content.ReadAsStringAsync();
            return (true, messageResult);
        }
        else
        {
            _logger.LogWarning("Error sending SMS for phone Verification, {ReasonPhrase}", result.ReasonPhrase);
            return (false, result.ReasonPhrase);
        }
    }

    public async Task<bool> CheckVerificationAsync(ApplicationUser user, string phoneNumber, string verificationCode)
    {
        return await _userManager
            .VerifyChangePhoneNumberTokenAsync(user, verificationCode, phoneNumber);
    }

    public async Task<(bool Success, string? Error)> EnableSms2FaAsync(ApplicationUser user, string phoneNumber)
    {
        var token = await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber);
        var message = $"Enable phone 2FA code: {token}";

        var ecallMessage = new EcallMessage
        {
            To = phoneNumber,
            From = _smsOptions.Sender,
            Content = new EcallContent
            {
                Text = message
            }
        };

        var result = await _httpClient.PostAsJsonAsync(Message, ecallMessage);

        if (result.IsSuccessStatusCode)
        {
            var messageResult = await result.Content.ReadAsStringAsync();
            return (true, messageResult);
        }
        else
        {
            _logger.LogWarning("Error sending SMS to enable phone 2FA, {ReasonPhrase}", result.ReasonPhrase);
            return (false, result.ReasonPhrase);
        }
    }
}

If you're using a different provider, you'd adjust the implementation accordingly.

For example, services like Twilio or Nexmo can be used to send SMS messages. We will implement a simple service that uses an HTTP client to send SMS messages.

Install HTTP Client for Sending SMS (e.g., Twilio or custom SMS provider).

dotnet add package Twilio

Create the SMS Provider: Create a class to handle SMS sending.

public class SmsProvider
{
    private readonly HttpClient _httpClient;
    private readonly string _twilioAccountSid = "your_twilio_account_sid";
    private readonly string _twilioAuthToken = "your_twilio_auth_token";
    private readonly string _twilioPhoneNumber = "your_twilio_phone_number";

    public SmsProvider(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<(bool Success, string? Error)> SendSmsAsync(string phoneNumber, string message)
    {
        var client = new TwilioRestClient(_twilioAccountSid, _twilioAuthToken);

        try
        {
            var messageResource = await MessageResource.CreateAsync(
                to: new PhoneNumber(phoneNumber),
                from: new PhoneNumber(_twilioPhoneNumber),
                body: message
            );

            return (true, null);
        }
        catch (Exception ex)
        {
            return (false, ex.Message);
        }
    }
}

Inject this service into the Startup.cs or Program.cs:

builder.Services.AddSingleton<SmsProvider>();

Verifying Phone

After a user authenticates with their email and password, they can verify their phone number. The verification process should always require the user to be authenticated to prevent malicious attempts to spam SMS requests.

The verification starts when the user clicks the "Add phone number" link, initiating the verification process.

var user = await _userManager.GetUserAsync(User);
if (user == null)
{
    return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

var result = await _client.StartVerificationAsync(user, Input.PhoneNumber);

The Razor page for verification lets the user input their phone number, sending an SMS with the StartVerificationAsync method. Once the user enters the received code, the VerifyAndProcessCode method validates the verification using VerifyChangePhoneNumberTokenAsync.

private async Task<IActionResult> VerifyAndProcessCode(string phoneNumber, string code)
{
    var applicationUser = await _userManager.GetUserAsync(User);

    if (applicationUser != null)
    {
        var validCodeForUserSession = await _client.CheckVerificationAsync(applicationUser,
            phoneNumber, code);

        return await ProcessValidCode(applicationUser, validCodeForUserSession);
    }
    else
    {
        ModelState.AddModelError("", "No user");
        return Page();
    }
}

Enabling SMS 2FA

Once the phone number is verified, SMS 2FA can be enabled.

var user = await _userManager.GetUserAsync(User);
if (user == null)
{
    return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}

await _smsVerifyClient.EnableSms2FaAsync(user, Input.PhoneNumber!);

The EnableSms2FaAsync method sends an SMS with a code to enable SMS-based 2FA. The user must confirm the code using VerifyChangePhoneNumberTokenAsync, and upon success, 2FA is activated.

await _userManager.SetTwoFactorEnabledAsync(user, true);

Using SMS for 2FA Authentication

After 2FA is enabled, users will need to verify via SMS for every login if SMS is enabled as the 2FA method.

if (user.Phone2FAEnabled)
{
    IsPhone = true;
    if (!user.AuthenticatorApp2FAEnabled)
    {
        await _smsVerifyClient.Send2FASmsAsync(user, user.PhoneNumber!);
    }
}

With these steps, you can implement phone verification and SMS-based 2FA using ASP.NET Core Identity. While SMS is less secure than other methods (like authenticator apps or FIDO2), it provides an effective way to secure user accounts for scenarios where other methods aren't available.