Two-Factor Authentication (2FA)
Two-Factor Authentication (2FA) is a Phase 2 (MVP 2) feature and is NOT available in MVP 1. This documentation is for future implementation planning.
Overview
Two-Factor Authentication adds an extra layer of security to user accounts by requiring a second form of verification beyond just the password. This significantly reduces the risk of unauthorized access even if a password is compromised.
Authentication Methods:
- Username/Password (something you know)
- Time-based One-Time Password - TOTP (something you have)
2FA Endpoints
Enable Two-Factor Authentication
Endpoint: POST /manage/2fa/enable
Headers:
Authorization: Bearer {access-token}
Request Body:
{
"password": "CurrentPassword123!"
}
Response: 200 OK
{
"success": true,
"data": {
"qrCodeUrl": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"manualEntryKey": "JBSWY3DPEHPK3PXP",
"recoveryCodes": [
"ABCD-1234-EFGH-5678",
"IJKL-9012-MNOP-3456",
"QRST-7890-UVWX-1234",
"YZAB-4567-CDEF-8901",
"GHIJ-2345-KLMN-6789"
]
},
"message": "Scan the QR code with your authenticator app and enter the verification code."
}
Description:
- Returns QR code for scanning with authenticator apps
- Provides manual entry key for apps that don't support QR codes
- Generates 5 recovery codes for emergency access
- User must verify with TOTP code to complete 2FA setup
Verify and Activate 2FA
Endpoint: POST /manage/2fa/verify
Headers:
Authorization: Bearer {access-token}
Request Body:
{
"twoFactorCode": "123456"
}
Response: 200 OK
{
"success": true,
"message": "Two-factor authentication has been enabled successfully.",
"data": {
"enabled": true,
"enabledAt": "2024-01-22T14:30:00Z"
}
}
Error Response: 400 Bad Request (Invalid Code)
{
"success": false,
"error": {
"code": "INVALID_2FA_CODE",
"message": "Invalid verification code. Please try again."
}
}
Disable Two-Factor Authentication
Endpoint: POST /manage/2fa/disable
Headers:
Authorization: Bearer {access-token}
Request Body:
{
"password": "CurrentPassword123!",
"twoFactorCode": "123456"
}
Response: 200 OK
{
"success": true,
"message": "Two-factor authentication has been disabled."
}
Error Response: 400 Bad Request
{
"success": false,
"error": {
"code": "INVALID_2FA_CODE",
"message": "Invalid verification code or password."
}
}
Get 2FA Status
Endpoint: GET /manage/2fa/status
Headers:
Authorization: Bearer {access-token}
Response: 200 OK
{
"success": true,
"data": {
"enabled": true,
"enabledAt": "2024-01-22T14:30:00Z",
"hasRecoveryCodes": true,
"recoveryCodesRemaining": 5
}
}
Generate New Recovery Codes
Endpoint: POST /manage/2fa/recovery-codes/generate
Headers:
Authorization: Bearer {access-token}
Request Body:
{
"password": "CurrentPassword123!"
}
Response: 200 OK
{
"success": true,
"data": {
"recoveryCodes": [
"ABCD-1234-EFGH-5678",
"IJKL-9012-MNOP-3456",
"QRST-7890-UVWX-1234",
"YZAB-4567-CDEF-8901",
"GHIJ-2345-KLMN-6789"
]
},
"message": "New recovery codes generated. Save these in a secure location."
}
Note: Generating new recovery codes invalidates all previous recovery codes.
Login Flow with 2FA
Standard Login with 2FA Enabled
Login with Recovery Code
If user loses access to their authenticator app, they can use a recovery code:
Endpoint: POST /login/verify-recovery-code
Request Body:
{
"email": "user@example.com",
"sessionToken": "temp-session-token-from-initial-login",
"recoveryCode": "ABCD-1234-EFGH-5678"
}
Response: 200 OK
{
"success": true,
"data": {
"tokenType": "Bearer",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"refreshToken": "refresh-token-here"
},
"message": "Login successful. You have 4 recovery codes remaining."
}
Note: Each recovery code can only be used once.
2FA Setup Flow
User Experience Flow
Supported Authenticator Apps
Recommended Apps:
- Google Authenticator (iOS, Android)
- Microsoft Authenticator (iOS, Android)
- Authy (iOS, Android, Desktop)
- 1Password (Cross-platform)
- LastPass Authenticator (iOS, Android)
TOTP Standard: RFC 6238
- 6-digit codes
- 30-second time window
- HMAC-SHA1 algorithm
Security Features
Recovery Codes
Features:
- 5 single-use recovery codes generated on 2FA activation
- Each code can only be used once
- Codes expire when new ones are generated
- Stored as hashed values in database
Best Practices:
- Save recovery codes in a secure password manager
- Print and store in a safe location
- Never share recovery codes with anyone
- Generate new codes if compromised
Rate Limiting
2FA Verification:
- Maximum 5 attempts per 15 minutes
- Account temporarily locked after 5 failed attempts
- 15-minute cooldown period
Recovery Code Usage:
- Maximum 3 attempts per hour
- Extended lockout (1 hour) after 3 failed attempts
Implementation Details
Backend (ASP.NET Core Identity)
NuGet Packages:
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="9.0.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />
2FA Service Implementation:
public class TwoFactorAuthService
{
private readonly UserManager<ApplicationUser> _userManager;
public async Task<TwoFactorSetupResult> EnableTwoFactorAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
// Generate authenticator key
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
// Generate QR code
var qrCodeUrl = GenerateQrCodeUri(user.Email, unformattedKey);
// Generate recovery codes
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 5);
return new TwoFactorSetupResult
{
QrCodeUrl = qrCodeUrl,
ManualEntryKey = FormatKey(unformattedKey),
RecoveryCodes = recoveryCodes.ToList()
};
}
public async Task<bool> VerifyTwoFactorCodeAsync(string userId, string code)
{
var user = await _userManager.FindByIdAsync(userId);
var isValid = await _userManager.VerifyTwoFactorTokenAsync(
user,
_userManager.Options.Tokens.AuthenticatorTokenProvider,
code
);
if (isValid)
{
await _userManager.SetTwoFactorEnabledAsync(user, true);
}
return isValid;
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
"otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6",
UrlEncoder.Encode("MicDots"),
UrlEncoder.Encode(email),
unformattedKey
);
}
}
Frontend Integration
React Example - Enable 2FA:
async function enableTwoFactor(password: string) {
try {
const response = await fetch("/manage/2fa/enable", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ password }),
});
const result = await response.json();
if (result.success) {
// Display QR code
setQrCodeUrl(result.data.qrCodeUrl);
// Display recovery codes
setRecoveryCodes(result.data.recoveryCodes);
// Show verification input
setShowVerificationStep(true);
}
} catch (error) {
showErrorMessage("Failed to enable 2FA");
}
}
async function verifyTwoFactor(code: string) {
try {
const response = await fetch("/manage/2fa/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ twoFactorCode: code }),
});
const result = await response.json();
if (result.success) {
showSuccessMessage("2FA enabled successfully!");
redirectToSettings();
} else {
showErrorMessage("Invalid verification code");
}
} catch (error) {
showErrorMessage("Verification failed");
}
}
Testing 2FA
Using cURL
Enable 2FA:
curl -X POST http://localhost:5000/manage/2fa/enable \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {access-token}" \
-d '{
"password": "CurrentPassword123!"
}'
Verify 2FA Code:
curl -X POST http://localhost:5000/manage/2fa/verify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {access-token}" \
-d '{
"twoFactorCode": "123456"
}'
Check 2FA Status:
curl -X GET http://localhost:5000/manage/2fa/status \
-H "Authorization: Bearer {access-token}"
Disable 2FA:
curl -X POST http://localhost:5000/manage/2fa/disable \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {access-token}" \
-d '{
"password": "CurrentPassword123!",
"twoFactorCode": "123456"
}'
Database Schema
Additional Fields for AspNetUsers Table
| Column | Type | Description |
|---|---|---|
| TwoFactorEnabled | boolean | Whether 2FA is enabled for this user |
| AuthenticatorKey | string | TOTP secret key (encrypted) |
| TwoFactorRecoveryCodesCount | int | Number of unused recovery codes |
AspNetUserTokens Table
Recovery codes are stored as tokens with:
LoginProvider: "RecoveryCodes"Name: Hashed recovery codeValue: Creation timestamp
User Experience Best Practices
Setup Process:
- Clear instructions on what 2FA is and why it's important
- Step-by-step setup wizard
- Prominent display of recovery codes with download option
- Verification step before activation
Ongoing Usage:
- Clear indication in UI that 2FA is enabled
- Easy access to regenerate recovery codes
- Option to disable 2FA (with password + current code)
- Login help for users locked out
Error Handling:
- Clear error messages for invalid codes
- Instructions for users who lost authenticator access
- Support contact information
- Account recovery process documentation
Related Documentation
- MVP 2 Overview - Complete MVP 2 features
- MVP 1 Authentication - Basic authentication
- Client Registration - User registration flow