A fully custom two-factor authentication sample project built with ASP.NET Core 10 MVC, ASP.NET Core Identity, Entity Framework Core, and Fido2.AspNet (WebAuthn / Passkeys). No scaffolded Identity UI — every page is hand-crafted. The dashboard uses the free AdminLTE 3 Bootstrap template.
| Feature | Details |
|---|---|
| User Registration | Custom form → confirmation page (no email confirmation required) |
| User Login | Email + password with lockout protection |
| Two-Factor Auth (TOTP) | QR code + manual key setup; compatible with Microsoft Authenticator, Google Authenticator, Authy |
| 2FA Login | Code prompt after password; "remember device" option |
| Recovery Codes | Generate 10 one-time codes; in-browser copy + download as .txt |
| Passkey Registration | WebAuthn / FIDO2 via Fido2NetLib; name your passkey |
| Passkey Login | Fully passwordless via device biometrics / security key |
| Dashboard | AdminLTE 3 sidebar layout with security overview |
| Profile | Account details and settings summary |
| Change Password | Current + new password with re-sign-in refresh |
| Access Denied | Custom 403 page |
| Dependency Injection | All services registered in Program.cs via built-in DI |
| EF Migrations | Bundled InitialCreate migration; EnsureCreated on startup |
TwoFactorAuth/
├── Controllers/
│ ├── AccountController.cs # Login, Register, 2FA, Passkey, Logout
│ ├── DashboardController.cs # Authenticated home
│ ├── HomeController.cs # Public landing page
│ └── SettingsController.cs # Profile, Password, 2FA, Recovery, Passkey
├── Data/
│ └── ApplicationDbContext.cs # EF DbContext (Identity + StoredCredentials)
├── Migrations/
│ ├── 20240101000000_InitialCreate.cs
│ └── ApplicationDbContextModelSnapshot.cs
├── Models/
│ ├── ApplicationUser.cs # Extended IdentityUser
│ └── StoredCredential.cs # FIDO2 passkey credential
├── Services/
│ ├── IPasskeyService.cs
│ └── PasskeyService.cs # Fido2 implementation
├── ViewModels/
│ ├── Account/AccountViewModels.cs
│ ├── Dashboard/DashboardViewModel.cs
│ └── Settings/SettingsViewModels.cs
├── Views/
│ ├── Account/ # Login, Register, Confirmation, 2FA, Passkey, AccessDenied
│ ├── Dashboard/ # AdminLTE dashboard
│ ├── Home/ # Public landing page + Error
│ ├── Settings/ # Profile, ChangePassword, TwoFactor, EnableAuthenticator,
│ │ # RecoveryCodes, Passkey
│ └── Shared/
│ ├── _Layout.cshtml # Public pages layout (Bootstrap 5)
│ ├── _DashboardLayout.cshtml # AdminLTE authenticated layout
│ └── _ValidationScriptsPartial.cshtml
├── appsettings.json
├── appsettings.Development.json
├── Program.cs # DI wiring, middleware pipeline
└── TwoFactorAuth.csproj
- .NET 10 SDK
- Any IDE: Visual Studio 2026, VS Code with C# extension, or JetBrains Rider
# Extract the zip, then:
cd TwoFactorAuthdotnet restoreSet your database connection string in appsettings.json.
The included migration will be applied automatically on first run via EnsureCreated() in Program.cs.
Alternatively, run it explicitly:
dotnet ef database updateNote: The database will be created automatically using the configured connection string.
dotnet run --launch-profile httpsThen open https://localhost:7246 in your browser or as per the setting in your project.
WebAuthn requires HTTPS (or localhost). The default launchSettings.json uses:
- HTTPS:
https://localhost:7246 - HTTP:
http://localhost:5015
If you change the port, update appsettings.json → Fido2:Origins to match:
"Fido2": {
"ServerDomain": "localhost",
"ServerName": "TwoFactorAuth",
"Origins": [ "https://localhost:YOUR_PORT" ]
}| Type | Examples |
|---|---|
| Platform (biometric) | Windows Hello, Face ID, Touch ID |
| Roaming (hardware key) | YubiKey, Google Titan |
| TOTP app | Microsoft Authenticator, Google Authenticator, Authy |
Home Page
├── Register → RegisterConfirmation → Login
└── Login
├── (2FA disabled) → Dashboard
├── (2FA enabled) → TwoFactorLogin → Dashboard
│ └── LoginWithRecoveryCode → Dashboard
└── PasskeyLogin → Dashboard
Dashboard (authenticated)
└── Settings
├── Profile Overview
├── Change Password
├── Two-Factor Auth
│ ├── Enable → EnableAuthenticator (QR Code) → RecoveryCodes
│ ├── Disable
│ └── Recovery Codes (View / Generate / Download)
└── Passkeys
├── Register new passkey (WebAuthn)
└── Delete passkey
- No scaffolded Identity UI — every
.cshtmlview and controller action is written from scratch. - SQLite used for simplicity; swap for SQL Server by replacing the
UseSqlitecall inProgram.cswithUseSqlServerand updating the connection string. - Session is used to pass the FIDO2 challenge between the options and verify steps (standard WebAuthn pattern).
EnsureCreated()is called at startup for zero-friction dev setup. For production, remove it and usedotnet ef database updatein your deployment pipeline.- Recovery codes are generated and shown only once (Identity behaviour). The download feature
uses the browser's Blob API to generate a
.txtfile client-side from the one-time display.
- Antiforgery tokens are enforced globally via
AutoValidateAntiforgeryTokenAttribute. - Passkey endpoints use
[IgnoreAntiforgeryToken]only where required by the WebAuthn fetch flow. - Cookie policy:
HttpOnly,SameSite,SecurePolicy.SameAsRequest. - Account lockout: 5 failed attempts → 5-minute lockout.
- Passwords: min 8 chars, uppercase + digit required.
This project is happily built and shared with the community to spread knowledge and learning.
If you like it or find it helpful, please consider giving it a ⭐ on GitHub.
