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.
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 type | Meaning | Everyday example |
|---|---|---|
| Something you know | A secret in your head | Password, PIN |
| Something you have | A physical thing you hold | Phone with an app, security key |
| Something you are | Part of your body | Fingerprint, 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.
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
Steps
Password
User enters email and password
Check 2FA
Is 2FA enabled for this user?
Code
User types the 6-digit app code
Logged in
Both factors passed, session created
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.
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.
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.
| Situation | What the user does |
|---|---|
| Normal login | Enter the 6-digit code from the app |
| Phone lost or dead | Enter one unused recovery code |
| Ran low on recovery codes | Regenerate a new set of 10 |
| New phone | Re-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: trueso 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
Steps
HTTPS
Encrypt everything in transit
Lockout
Stop brute-force code guessing
Recovery
Backup codes, shown once
App over SMS
TOTP is safer and free
Reset path
Re-enroll on a trusted page
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.
AddDefaultIdentityregisters the authenticator token provider throughAddDefaultTokenProviders. - 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 callingSetTwoFactorEnabledAsync. - Login is two steps.
PasswordSignInAsyncreturnsRequiresTwoFactor; thenTwoFactorAuthenticatorSignInAsyncfinishes 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
- Multi-factor authentication in ASP.NET Core — Microsoft Learn
- Enable QR code generation for TOTP authenticator apps — Microsoft Learn
- How to Implement Two-Factor Authentication in ASP.NET Core — Milan Jovanović
- RFC 6238 — TOTP: Time-Based One-Time Password Algorithm
Related Posts
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.
Refresh Tokens and Token Revocation in ASP.NET Core: A Beginner Guide
Learn refresh tokens and token revocation in ASP.NET Core with simple words, diagrams, and code. Short access tokens, rotation, reuse detection, and safe logout.
API Key Authentication in ASP.NET Core: The Secure Way
Learn how to add API key authentication to your ASP.NET Core API the right way. Use an AuthenticationHandler, hash keys, compare safely, and follow 2026 security best practices, with diagrams and code.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
HTTPS Redirection and HSTS in ASP.NET Core: A Simple Guide
Learn how to configure HTTPS redirection and HSTS in ASP.NET Core with simple examples, diagrams, and clear advice for development and production.
CORS in ASP.NET Core: A Comprehensive Guide
A simple, friendly guide to CORS in ASP.NET Core. Learn how the browser, preflight requests, and policies work, with clear diagrams, tables, and code.