Skip to main content
SEMastery
ASP.NETintermediate

How to Implement Two-Factor Authentication in ASP.NET Core

Learn how to add two-factor authentication (2FA) to ASP.NET Core with Identity and TOTP authenticator apps. QR codes, recovery codes, diagrams, and clear code.

11 min readUpdated February 12, 2026

A locker with two keys

Think about a bank locker. To open it, the bank manager turns their key, and you turn your key. One key alone does nothing. You need both at the same time.

A password is just one key. If someone peeks over your shoulder, guesses it, or buys it from a leaked database, they can walk straight into your account. That is scary.

Two-factor authentication (2FA) adds a second key. The first key is your password — something you know. The second key is a short code from an app on your phone — something you have. A thief might steal your password, but they do not have your phone. Without both keys, the locker stays shut.

In this guide we will add 2FA to an ASP.NET Core app using Identity and TOTP authenticator apps like Google Authenticator or Microsoft Authenticator. No SMS, no extra cost, and it follows the way Microsoft recommends in 2026.

The two factors, simply

A "factor" is just a category of proof. Security people group proof into three buckets.

Factor typeMeaningEveryday example
Something you knowA secret in your headPassword, PIN
Something you haveA physical thing you holdPhone with an app, security key
Something you arePart of your bodyFingerprint, face scan

Two-factor authentication means using two different buckets together. A password plus a second password is not 2FA — that is the same bucket twice. A password (know) plus a phone code (have) is real 2FA. That mix is what makes it strong.

What is TOTP?

TOTP stands for Time-based One-Time Password. It is the engine behind those 6-digit codes that change every 30 seconds.

Here is the idea. When you turn on 2FA, the server creates a secret key and shares it once with your phone (usually by scanning a QR code). After that, both the server and your phone do the same little maths: they take the shared secret plus the current time, and produce a 6-digit code. Because they share the secret and both know the time, they always agree on the code — without ever talking to each other again.

Figure 1: The server and the phone share a secret once. After that, both make the same 6-digit code from secret plus time.

The code changes every 30 seconds, so even if someone sees an old code, it is useless a moment later. TOTP is an open standard (RFC 6238), which is why one app works with thousands of websites.

The full sign-in flow with 2FA

Let us see the whole journey. The user first signs in with their password as usual. If their account has 2FA turned on, Identity does not log them in yet. Instead it asks for the second factor — the code from the app.

Sign-in with 2FA

Password
Check 2FA
Code
Logged in

Steps

1

Password

User enters email and password

2

Check 2FA

Is 2FA enabled for this user?

3

Code

User types the 6-digit app code

4

Logged in

Both factors passed, session created

The password is step one. The authenticator code is step two. Both must pass.

The important part is that the password step alone never finishes the login. Only after the second factor is also correct does the user get a real signed-in session.

Figure 2: A sequence view of a 2FA login. Notice the login is held until the code is verified.

Setting up Identity with 2FA support

Most of the heavy lifting is already built into ASP.NET Core Identity. When you call AddDefaultIdentity (or AddIdentity), it internally calls AddDefaultTokenProviders, which registers the authenticator TOTP provider for you. You usually do not need to add anything special just to support 2FA.

Here is a typical Program.cs for an app using Identity with EF Core.

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
 
builder.Services
    .AddDefaultIdentity<IdentityUser>(options =>
    {
        // Make accounts a little stricter.
        options.SignIn.RequireConfirmedAccount = true;
        options.Password.RequiredLength = 8;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>();
    // AddDefaultTokenProviders() is called for you, which
    // includes the authenticator (TOTP) token provider.
 
var app = builder.Build();
 
app.UseAuthentication();
app.UseAuthorization();
 
app.MapRazorPages();
app.Run();

If you use AddIdentity instead of AddDefaultIdentity, make sure you also call .AddDefaultTokenProviders() yourself. Without it, the authenticator codes cannot be generated or verified.

Step 1: Show the user a QR code to enroll

To turn on 2FA, the user must teach their authenticator app the shared secret. The friendly way to do this is a QR code. The user opens their app, taps "scan", and points the camera at the screen. The app reads the secret and starts making codes.

Identity gives you the secret with GetAuthenticatorKeyAsync. If the user does not have one yet, you create it with ResetAuthenticatorKeyAsync. You then build a special otpauth:// URI that the QR code will encode.

public async Task<(string sharedKey, string qrCodeUri)> GetEnrollDataAsync(
    UserManager<IdentityUser> userManager, IdentityUser user)
{
    // Make sure the user has an authenticator key.
    var key = await userManager.GetAuthenticatorKeyAsync(user);
    if (string.IsNullOrEmpty(key))
    {
        await userManager.ResetAuthenticatorKeyAsync(user);
        key = await userManager.GetAuthenticatorKeyAsync(user);
    }
 
    var email = await userManager.GetEmailAsync(user);
 
    // This URI is what the QR code encodes.
    var uri = string.Format(
        "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6",
        Uri.EscapeDataString("MyApp"),
        Uri.EscapeDataString(email!),
        key);
 
    return (FormatKey(key!), uri);
}
 
// Show the key in neat 4-letter groups so a human can type it too.
private static string FormatKey(string key)
{
    var result = new System.Text.StringBuilder();
    for (var i = 0; i < key.Length; i += 4)
        result.Append(key.AsSpan(i, Math.Min(4, key.Length - i))).Append(' ');
    return result.ToString().ToLowerInvariant().Trim();
}

You then turn that qrCodeUri into an actual picture. A small JavaScript library like qrcode.js can draw it in the browser, or you can render it server-side. Always show the plain text key too, in case the user cannot scan.

Step 2: Verify the first code before turning 2FA on

Do not trust that the scan worked — ask the user to prove it. They type the first 6-digit code their app shows, and you verify it. Only if it matches do you actually enable 2FA. This catches setup mistakes early.

public async Task<bool> EnableTwoFactorAsync(
    UserManager<IdentityUser> userManager, IdentityUser user, string codeFromUser)
{
    // Clean the input: remove spaces and dashes.
    var code = codeFromUser.Replace(" ", string.Empty).Replace("-", string.Empty);
 
    var isValid = await userManager.VerifyTwoFactorTokenAsync(
        user,
        userManager.Options.Tokens.AuthenticatorTokenProvider,
        code);
 
    if (!isValid)
        return false; // Wrong code. Ask them to try again.
 
    await userManager.SetTwoFactorEnabledAsync(user, true);
    return true;
}

If VerifyTwoFactorTokenAsync returns false, the code was wrong or the phone clock was off. Ask the user to try again with a fresh code. If it returns true, you call SetTwoFactorEnabledAsync, and from now on this user needs the second factor to log in.

Figure 3: The enrollment state machine. 2FA only turns on after the very first code is verified.

Step 3: Handle the login with 2FA

Now the real payoff. When a user signs in, use PasswordSignInAsync. If the password is right and 2FA is on, Identity returns a result where RequiresTwoFactor is true. That is your signal to send them to a second page asking for the code.

var result = await signInManager.PasswordSignInAsync(
    email, password, isPersistent: false, lockoutOnFailure: true);
 
if (result.RequiresTwoFactor)
{
    // Password was correct, but we still need the second factor.
    return RedirectToPage("./LoginWith2fa", new { rememberMe = false });
}
 
if (result.Succeeded)
{
    // No 2FA on this account; the user is fully logged in.
    return LocalRedirect("/");
}
 
if (result.IsLockedOut)
{
    return RedirectToPage("./Lockout");
}

On the second page, the user enters the code, and you finish the login with TwoFactorAuthenticatorSignInAsync.

var code = inputCode.Replace(" ", string.Empty).Replace("-", string.Empty);
 
var result = await signInManager.TwoFactorAuthenticatorSignInAsync(
    code, isPersistent: rememberMe, rememberClient: rememberMachine);
 
if (result.Succeeded)
    return LocalRedirect("/"); // Both factors passed.
 
if (result.IsLockedOut)
    return RedirectToPage("./Lockout");
 
// Wrong code.
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");

The rememberClient flag is a nice touch. If the user ticks "remember this device", Identity drops a cookie so they will not be asked for the code again on that same browser for a while. Use it carefully — only on devices the user trusts.

Step 4: Give the user recovery codes

What if the user drops their phone in a river? Without a backup, they would be locked out forever. So when 2FA is enabled, generate recovery codes — one-time backup codes the user saves somewhere safe.

var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(
    user, number: 10);
 
// Show these once. Tell the user to store them safely
// (password manager, printed paper). They will not see them again.
foreach (var oneCode in recoveryCodes!)
{
    // Display each code to the user here.
}

Each code works once. If the user is stuck without their phone, they log in with a recovery code instead of the app code, using TwoFactorRecoveryCodeSignInAsync. After they use a few, remind them to generate a fresh batch.

SituationWhat the user does
Normal loginEnter the 6-digit code from the app
Phone lost or deadEnter one unused recovery code
Ran low on recovery codesRegenerate a new set of 10
New phoneRe-scan the QR code to re-enroll

A few safety rules to remember

A few small rules keep your 2FA strong and your users happy.

  • Always use HTTPS. Codes and cookies must never travel in plain text. Pair 2FA with HTTPS redirection and HSTS.
  • Enable lockout. Set lockoutOnFailure: true so attackers cannot guess codes forever. A 6-digit code is only a million guesses, which is fast for a computer if you let them try without limit.
  • Show recovery codes only once. Treat them like a password. Store only hashed versions, which Identity already does.
  • Prefer authenticator apps over SMS. SMS can be intercepted or hit by SIM-swap attacks. TOTP apps are free and safer.
  • Let users turn 2FA off and reset it from a trusted, already-authenticated page — but make that page itself require a fresh login.

2FA security checklist

HTTPS
Lockout
Recovery
App over SMS
Reset path

Steps

1

HTTPS

Encrypt everything in transit

2

Lockout

Stop brute-force code guessing

3

Recovery

Backup codes, shown once

4

App over SMS

TOTP is safer and free

5

Reset path

Re-enroll on a trusted page

Five habits that keep two-factor authentication strong.

Common questions while building

The user typed the right code but it fails. This is almost always a clock problem. TOTP depends on time. If the phone or the server clock drifts by more than about 90 seconds, the codes will not match. Make sure your server uses a time-sync service (NTP), and tell users to enable automatic time on their phone.

Should I store the secret myself? No. Identity stores the authenticator key for you in the user store. Do not roll your own secret storage — you will likely get the encryption wrong.

Can I force 2FA for everyone? Yes. You can add a check that redirects users without 2FA to the setup page before they reach protected areas. This is common for admin panels and banking-style apps. Just make sure the enrollment page itself is reachable, or users will get stuck in a loop.

Quick recap

  • 2FA = two different kinds of proof. A password you know, plus a phone code you have. Like a locker that needs two keys.
  • TOTP is the standard behind authenticator apps. The server and phone share a secret once, then both make a matching 6-digit code from secret plus time, changing every 30 seconds.
  • ASP.NET Core Identity has 2FA built in. AddDefaultIdentity registers the authenticator token provider through AddDefaultTokenProviders.
  • Enrollment is a QR code. Build an otpauth:// URI from the user's authenticator key, show it as a QR code, and verify the first code before calling SetTwoFactorEnabledAsync.
  • Login is two steps. PasswordSignInAsync returns RequiresTwoFactor; then TwoFactorAuthenticatorSignInAsync finishes it once the code checks out.
  • Always issue recovery codes so a lost phone does not lock people out forever.
  • Stay safe: HTTPS, lockout on failure, prefer apps over SMS, and keep server clocks in sync.

References and further reading

Related Posts