How to Build Passwordless Auth Using TOTP With Twilio Verify in .NET

August 14, 2026
Written by

How to Build Passwordless Auth Using TOTP With Twilio Verify in .NET

Typing or generating a unique password for every new account or website is a hassle. Although password managers help, passwordless auth (authentication) offers a more streamlined and secure alternative.

In this tutorial, you will learn about passwordless auth, and build a .NET application that uses Twilio Verify to implement it.

Programming language support

This tutorial is geared toward .NET developers. If you would like to build this project in a different programming language, see the following options:

Prerequisites

Before you begin, ensure you have the following:

  • A free Twilio account. Click here to create a free account if you are new to Twilio.
  • An authentication app, such as Twilio Authy
  • .NET version 9 or later
  • Your favourite text editor or IDE (such as Visual Studio Code)
  • Your favourite web browser

Architecture

This application will be a simplistic web-based application, split up over three stages.

  • Stage one: the user will submit their username to begin setting up Two-factor Authentication (2FA).
  • Stage two: they'll set up a TOTP entry in their authenticator app. They'll do this with their authenticator app by scanning a QR code or entering a unique code. Then, they'll submit the initial code that their authenticator app generates for them. If the code is valid, their account is ready to use.
  • Stage three: They can validate future codes which their authenticator app generates for them in the final form in the application.

What is passwordless auth?

Passwordless auth is an authentication method where the user does not need a password in order to log into an app or system. Rather, the user's mobile device receives a one-time code. In this authentication method, users are authenticated using other unique and more secure alternatives like Time-based One-time Passwords (TOTP, or Soft Token), SMS, Passkeys, Silent Network Authentication (SNA), Voice, or email notification.

Here's a breakdown of how it works, using TOTP:

  1. User initiation: When a user creates an account, instead of entering a password, they create a new Factor resource with Twilio Verify — initially marked as unverified — seeding it with a unique (auto-generated) identifier.
  2. Scan the QR code with an authenticator app: Using an authenticator app such as Twilio Authy, they scan the QR code then enter the code that the authenticator app provides. The code is validated using Twilio Verify. If the code is valid the Factor is marked verified. The authenticator app can now provide time-based codes to use in the future, when logging in, which Twilio Verify will verify.
  3. Code validation: When the user logs in, they enter their username and a TOTP code generated by their authenticator app. The application then uses Twilio Verify to validate the code along with their unique identifier.

This approach significantly enhances security by leveraging the inherent security features of the user's mobile device as a second factor in the authentication process. It also eliminates the need for them to memorize passwords, thereby simplifying the authentication process and enhancing user convenience.

Passwordless authentication has additional advantages, including the following:

  • Enhanced security: Reduces the risk of password-related breaches.
  • Convenience: Eliminates the need for users to remember and manage multiple passwords.
  • Reduced friction: Streamlines the login process, improving the user experience.
  • Scalability: Easily scalable with Twilio's infrastructure.

Build the app

Step 1: Set up the project

Set up a new project by creating it in your IDE of choice, or by running the following command in the folder where you store your .NET projects:

dotnet new razor -n TwilioVerifyTotp 
cd TwilioVerifyTotp

Step 2: Install the required dependencies

Next, install the necessary nuget packages, by typing the following:

dotnet add package Twilio 
dotnet add package QRCoder 
dotnet add package DotNetEnv

In case you're not familiar with them, here's a short description of the dependencies that you just installed:

  • Twilio's .NET Helper Library: This simplifies integrating with Twilio in .NET
  • DotNetEnv: Loads environment variables from .env into your solution
  • QRCoder: The most popular and free open-source .NET package for generating QR codes.

Now, open the project directory in your preferred text editor or IDE.

Step 3: Set the required environment variables

Dotenv files (commonly named .env) are used to store the configuration information that your app needs during development, separate from the application's code. For this tutorial, it will be your Twilio credentials (i.e., your Twilio Account SID and Auth Token) and a Verify Service SID.

In your project's top-level directory, create a file called .env. Add the following variables to the file:

TWILIO_ACCOUNT_SID= 
TWILIO_AUTH_TOKEN= 
TWILIO_VERIFY_SERVICE_SID=

You next need to retrieve the credentials to set as their values. To do that, sign into the Twilio Console. There, click the black and white up arrow at the bottom of the page, and you should see your Account SID and Auth Token in the Workbench; as shown in the image below.

Dashboard interface showing API credentials, quick actions, and various tabs like overview, debugger, and alarms.

Copy these values and paste them into .env as the values for TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.

Next, navigate to Products and Services > Verify > Services. On this page, click Create new. Then, fill out the initial form with the configuration values shown in the screenshot below, and click Continue.

Twilio interface showing options to create a new verification service with channels like SMS, WhatsApp, Email, and Voice.

In the next step, leave Enable Fraud Guard set to "Yes" and click Continue to finish creating the service.

Popup window for enabling Fraud Guard with options to select Yes or No in an online dashboard.

After creating the service, copy the Service SID and paste it into .env as the value of TWILIO_VERIFY_SERVICE_SID.

Service settings page with options for Twilio Verify featuring SMS, WhatsApp, email, voice, and more.

Step 4: Build the base .NET application

The next task is to set up your Program.cs file. This will load in the environment variables, initialize the Twilio client, register Razor pages, and set up static files to serve HTML to your users. To do that, replace the code in your existing Program.cs file with the code below:

using DotNetEnv;
using Twilio;

Env.TraversePath().Load();

foreach (var name in new[] { "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_VERIFY_SERVICE_SID" })
{
    if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(name)))
    {
        throw new InvalidOperationException($"Required environment variable '{name}' is not set. Copy .env.example to .env and fill it in.");
    }
}

TwilioClient.Init(
    Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID"),
    Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN"));

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.ConfigureFilter(new Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute());
});
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.Cookie.Name = "app_session";
    options.IdleTimeout = TimeSpan.FromHours(1);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.MapRazorPages();

app.Run();

The updated code, above, loads environment variables from the variables defined in .env, using .NET dotenv, ensuring that TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_VERIFY_SERVICE_SID have been set and are not empty.

Then, it initializes the Razor pages that will be used to create a web interface for the demo, and maps those Razor pages to static templates that you will provide. Move on to creation of those templates in the next step.

Step 5: Build the shared layout and add CSS

All the pages for your demo will have a shared layout and CSS file that they use for formatting and display.

In your project folders, locate the file Pages/Shared/layout.cshtml. Paste in this code to replace the existing file:

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8">
   <title>@ViewData["Title"]</title>
   <link href="https://assets.twilio.com/public_assets/paste-fonts/1.5.2/fonts.css" rel="stylesheet">
   <link href="~/css/styles.css" rel="stylesheet">
</head>
<body class="container">
   <main>
       @RenderBody()
   </main>
</body>
</html>

This file creates a simple template that will bring in Twilio's fonts and your stylesheet for all of your pages.

To simplify this tutorial, the full style sheet won't be replicated here. You can download the application's CSS file from the project's GitHub repository. Search for your project's wwwroot/css directory, and add this file to it, naming it styles.css.

 

Step 6: Build the username registration page

Now it is time to create the pages that will make up your Razor web site.

Open your Index.cshtml file and paste in the following code:

@page
@model IndexModel
@{
    ViewData["Title"] = "Register Username";
}

<h1>Register Username</h1>
<p class="mt-60">
    This demo of the <a href="https://www.twilio.com/docs/verify/api">Twilio Verify API</a> shows how to set up two-factor authentication (2FA) with a third-party authenticator app like <a href="https://authy.com/download/">Authy</a> or Google Authenticator using the <a href="https://www.twilio.com/docs/glossary/totp">time-based one-time password (TOTP)</a> standard.
</p>
<hr class="mt-60">
<form action="/" method="post" class="mt-60 mb-60">
    <label for="username">Enter your username:</label>
    <div class="mt-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
        <input id="username" inputmode="text" name="username" required style="flex-shrink: 1;" type="text">
        <button class="primary" style="flex-grow: 1; white-space: nowrap" type="submit">Set up two-factor authentication</button>
    </div>
</form>
<div style="background-color: #d6ebf0; padding: 2em; border-radius: 0.25rem;" class="mb-60 mt-60">
    <h2 class="mb-60">Create a New Factor | What's happening here:</h2>
    <p style="line-height: 1.5;" class="mb-60">
        The API is <strong>creating a new TOTP <a href="https://www.twilio.com/docs/verify/api/factor#create-a-new-factor-resource">Factor</a></strong> when you click "Set up two-factor authentication".
        This is how the Verify API connects a user (<code>identity</code>), the <a href="https://www.twilio.com/docs/glossary/totp">TOTP channel</a>, and your app.
        Each Factor returns the secret seed and URI used to create a QR code.
    </p>
    <p>
        For this demo, we're asking for a username that will be displayed in the account name for the authenticator app.
    </p>
</div>
<a href="/">Reset</a>

As you can see from the HTML above, it is a simplistic HTML page with a form, with a single field named "username". Note that the field's type and inputmode attributes are set to "text". This hints to browsers on mobile devices to render a virtual keyboard most appropriate for entering text input.

Now open the file named Index.cshtml.cs. Put the following code in that file:

using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json.Linq;
using Twilio.Rest.Verify.V2.Service.Entity;

namespace TwilioVerifyTotp.Pages;

public class IndexModel : PageModel
{
    private readonly string _verifyServiceSid =
        Environment.GetEnvironmentVariable("TWILIO_VERIFY_SERVICE_SID")!;

    public void OnGet()
    {
    }

    public IActionResult OnPost(string? username)
    {
        var seed = Convert.ToHexString(RandomNumberGenerator.GetBytes(20))
            .ToLowerInvariant()
            .Substring(0, 32);

        HttpContext.Session.SetString("seed", seed);

        var factor = NewFactorResource.Create(
            pathServiceSid: _verifyServiceSid,
            pathIdentity: seed,
            friendlyName: username ?? string.Empty,
            factorType: NewFactorResource.FactorTypesEnum.Totp);

        HttpContext.Session.SetString("friendly_name", factor.FriendlyName ?? string.Empty);
        HttpContext.Session.SetString("sid", factor.Sid ?? string.Empty);
        HttpContext.Session.SetString("url", factor.Url?.ToString() ?? string.Empty);
        HttpContext.Session.SetString("otp_uri", ExtractOtpUri(factor.Binding));

        return Redirect(factor.Status == NewFactorResource.FactorStatusesEnum.Unverified
            ? "/challenge"
            : "/");
    }

    private static string ExtractOtpUri(object? binding)
    {
        if (binding is null) return string.Empty;
        var token = binding as JToken ?? JToken.FromObject(binding);
        return token["uri"]?.ToString() ?? string.Empty;
    }
}

The NewFactorResource.Create() method creates a new TOTP Factor with Twilio Verify. The factor is seeded by a string created with random bytes. This is then converted to hex format, of which the first 32 characters are retrieved, keeping it within the 8 - 64 character limit. As this is required later in the application, it's then stored in the current session.

Following this, it's used to create a new factor resource using the Twilio Rest Client. From the response to that request the following properties are stored in session for later use, the:

  • Friendly Name: The Factor's friendly name
  • SID: A 34 character string that uniquely identifies the Factor.
  • OTP URI: This stores the OTP configuration, including a shared secret and related parameters, for the OTP client (e.g., Authy) to use to generate the initial TOTP code.
  • URL:The URL of the Factor resource.

Then, if the new Factor's status is set to "unverified", the user is redirected to the verify-totp route. Otherwise, they're redirected back to the create-totp route to try again.

The ExtractOtpUri() method is a small utility method for getting the necessary URI formatted correctly for the OTP URI that you will connect to.

Step 7: Build the QR Code and Verify pages

You'll now add the functionality to render the form with the QR code for verifying the new Factor resource. Create a new file in your project called Challenge.cshtml.cs. Paste in the following code:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using QRCoder;
using Twilio.Rest.Verify.V2.Service.Entity;

namespace TwilioVerifyTotp.Pages;

public class ChallengeModel : PageModel
{
    private readonly string _verifyServiceSid =
        Environment.GetEnvironmentVariable("TWILIO_VERIFY_SERVICE_SID")!;

    public string QrCode { get; private set; } = string.Empty;
    public string Seed { get; private set; } = string.Empty;

    public void OnGet()
    {
        var otpUri = HttpContext.Session.GetString("otp_uri") ?? string.Empty;
        Seed = HttpContext.Session.GetString("seed") ?? string.Empty;
        QrCode = BuildQrCodeDataUri(otpUri);
    }

    public IActionResult OnPost(string? code)
    {
        var seed = HttpContext.Session.GetString("seed") ?? string.Empty;
        var sid = HttpContext.Session.GetString("sid") ?? string.Empty;

        var factor = FactorResource.Update(
            pathServiceSid: _verifyServiceSid,
            pathIdentity: seed,
            pathSid: sid,
            authPayload: code ?? string.Empty);

        if (factor.Status == FactorResource.FactorStatusesEnum.Verified)
        {
            TempData["message"] = "Factor setup complete!";
            return Redirect("/token");
        }

        return Redirect("/challenge");
    }

    private static string BuildQrCodeDataUri(string payload)
    {
        if (string.IsNullOrEmpty(payload))
        {
            return string.Empty;
        }

        using var generator = new QRCodeGenerator();
        using var data = generator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.M);
        var png = new PngByteQRCode(data).GetGraphic(10);
        return "data:image/png;base64," + Convert.ToBase64String(png);
    }
}

The template will render two variables:

  • A QR code which embeds the unverified Factor's OTP URI
  • The unverified Factor's seed or identity

With that done, in your project directory create a new file named Challenge.cshtml and paste the code below into the file.

@page
@model ChallengeModel
@{
    ViewData["Title"] = "Register User";
}

<h1>Register User</h1>
<p class="mt-60 mb-60">
    This demo of the <a href="https://www.twilio.com/docs/verify/api">Twilio Verify API</a> shows how to set up two-factor authentication (2FA) with a third-party authenticator app like <a href="https://authy.com/download/">Authy</a> or Google Authenticator using the <a href="https://www.twilio.com/docs/glossary/totp">time-based one-time password (TOTP)</a> standard.
</p>
<hr>
<form action="/challenge" method="post" class="mt-60">
    <p>Please scan the QR code in an authenticator app like Authy.</p>
    <div class="mt-20 mb-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
        <img src="@Model.QrCode" alt="QR Code" width="40%">
        <div class="mt-60">
            <p>Or enter this code into your authentication app:</p>
            <p><strong>@Model.Seed</strong></p>
        </div>
    </div>
    <label for="code">Enter the code generate by your authenticator app to verify this factor:</label>
    <div class="mt-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
        <input id="code" inputmode="numeric" name="code" required pattern="\d{6}" placeholder="123456" type="text">
        <button type="submit" class="primary" style="flex-grow: 1;">Verify</button>
    </div>
</form>
<div style="background-color: #d6ebf0; padding: 2em; border-radius: 0.25rem;" class="mb-60">
    <h2 class="mb-60">Create a New Factor | What's happening here:</h2>
    <p style="line-height: 1.5;" class="mb-60">
        The API is <strong>creating a new TOTP <a href="https://www.twilio.com/docs/verify/api/factor#create-a-new-factor-resource">Factor</a></strong> when you click "Set up two-factor authentication".
        This is how the Verify API connects a user (<code>identity</code>), the <a href="https://www.twilio.com/docs/glossary/totp">TOTP channel</a>, and your app.
        Each Factor returns the secret seed and URI used to create a QR code.
    </p>
    <p>
        For this demo, we're asking for a username that will be displayed in the account name for the authenticator app.
    </p>
</div>
<a href="/">Reset</a>

The HTML renders another, simplistic, form with the QR code to scan, and a field for the TOTP code generated by the user's authenticator app. The field uses the pattern attribute to ensure that the only valid input is a 6-digit code. It also sets the inputmode attribute to "numeric" to have mobile browsers display a keyboard appropriate for entering digits, making it easier for the user to only enter numeric input.

If the Factor is successfully verified, a confirmation message is flashed and the user is redirected to the route where they can enter QR codes generated by their application post-Factor creation, where the flashed message will be displayed, confirming that the Factor was successfully verified. Otherwise, they'll be redirected back to the verify TOTP Factor stage, to scan the QR code and try again.

Step 8: Add the ability to validate TOTP codes after Factor verification

Now, you'll add the third and final feature: the ability to validate TOTP codes, post-Factor verification. Add a file to your project called Token.cshtml.cs. Paste in this code:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Twilio.Rest.Verify.V2.Service.Entity;

namespace TwilioVerifyTotp.Pages;

public class TokenModel : PageModel
{
    private readonly string _verifyServiceSid =
        Environment.GetEnvironmentVariable("TWILIO_VERIFY_SERVICE_SID")!;

    public string FriendlyName { get; private set; } = string.Empty;
    public string Seed { get; private set; } = string.Empty;
    public string? Message { get; private set; }

    public void OnGet()
    {
        FriendlyName = HttpContext.Session.GetString("friendly_name") ?? string.Empty;
        Seed = HttpContext.Session.GetString("seed") ?? string.Empty;
        Message = TempData["message"] as string;
    }

    public IActionResult OnPost(string? code)
    {
        var seed = HttpContext.Session.GetString("seed") ?? string.Empty;
        var sid = HttpContext.Session.GetString("sid") ?? string.Empty;

        var challenge = ChallengeResource.Create(
            pathServiceSid: _verifyServiceSid,
            pathIdentity: seed,
            factorSid: sid,
            authPayload: code ?? string.Empty);

        TempData["message"] = challenge.Status == ChallengeResource.ChallengeStatusesEnum.Approved
            ? "Verification success."
            : "Verification failed.";

        return Redirect("/token");
    }
}

The template initially renders an HTML form where the user can enter and submit a TOTP code generated by their authenticator app. On form submission, the application verifies the code with Twilio Verify.

If Twilio Verify marks the code as "approved", then a flash message confirming that is flashed. Otherwise, a message confirming that the code was invalid is flashed. The user is then redirected back to the TOTP code form, where the message will be displayed.

Now, create a new file named Token.cshtml, and paste the code below into the file.

@page
@model TokenModel
@{
    ViewData["Title"] = "Enter TOTP Code";
}

<h1>Enter TOTP Code</h1>
<p class="mt-60">
    This demo of the <a href="https://www.twilio.com/docs/verify/api">Twilio Verify API</a> shows how to set up two-factor authentication (2FA) with a third-party authenticator app like <a href="https://authy.com/download/">Authy</a> or Google Authenticator using the <a href="https://www.twilio.com/docs/glossary/totp">time-based one-time password (TOTP)</a> standard.
</p>
<hr class="mt-60">
@if (!string.IsNullOrEmpty(Model.Message))
{
    var alertType = Model.Message is "Verification success." or "Factor setup complete!" ? "success" : "error";
    <p class="mt-60 alert alert-@alertType">@Model.Message</p>
}
<form action="/token" method="post" class="mt-60 mb-100">
    <label for="code">Enter the generated by your authenticator app:</label>
    <div class="mt-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
        <input id="code" inputmode="numeric" name="code" required pattern="\d{6}" placeholder="123456" type="text">
        <button type="submit" class="primary" style="flex-grow: 1;">Verify</button>
    </div>
</form>
<p>
    Demo is running for username '@Model.FriendlyName' with identity '@Model.Seed'.
    <a href="/token">Validate a code</a> or <a href="/">Reset</a>
</p>

Similar to the previous template, this one renders a form with a field where the user can input and submit the code which their authenticator app generates. It also provides a link to start over and create a new Factor.

Test that the application works

Finally, it's time to test that the code works as expected. Start the application by running the following command.

dotnet run

Your server will now start, as shown by terminal output similar to the following. Your port number may vary, so check the output for the right URL.

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000

You can now navigate to http://localhost:5000 (or the URL provided) to test the TOTP Factor creation process.

Webpage showing a form for setting up two-factor authentication with Twilio Verify API.

Enter a username of your choice and click Set up two-factor authentication.

Webpage showing instructions for setting up new TOTP factor, with QR code, input field, and steps to follow.

You will be redirected to the Verify New TOTP Factor form. With your authentication app, scan the QR code, enter the 6-digit code that it generates into the form, and click Verify.

Screen showing TOTP code entry for two-factor authentication with a verification button.

You'll then be redirected to the Enter TOTP Code form, where you'll see whether the Factor was set up successfully or not, as in the screenshot above.

Webpage showing input field for TOTP code verification with a success message and a Verify button.

If the Factor was set up successfully, enter the next 6-digit code into the Enter TOTP Code form and click Verify. Again, you should see if it was successfully verified or not, as in the screenshot above.

That's the essentials of implementing passwordless authentication in .NET using Twilio Verify

TOTP-based passwordless authentication using .NET and Twilio Verify presents a compelling alternative to traditional password-based systems.

It offers a blend of enhanced security and user convenience, making it an attractive option for modern applications. While passwordless auth introduces some extra complexity to your application, the benefits — especially regarding security and user experience — are worth the investment.

Amanda Lange is a .NET Engineer of Technical Content. She is here to teach how to create great things using C# and .NET programming. She can be reached at amlange [ at] twilio.com .

Otp icon created by Magnific on Flaticon.