Skip to main content

Two-Factor Authentication (2FA)

MVP 2 Feature

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​

ColumnTypeDescription
TwoFactorEnabledbooleanWhether 2FA is enabled for this user
AuthenticatorKeystringTOTP secret key (encrypted)
TwoFactorRecoveryCodesCountintNumber of unused recovery codes

AspNetUserTokens Table​

Recovery codes are stored as tokens with:

  • LoginProvider: "RecoveryCodes"
  • Name: Hashed recovery code
  • Value: Creation timestamp

User Experience Best Practices​

Setup Process:

  1. Clear instructions on what 2FA is and why it's important
  2. Step-by-step setup wizard
  3. Prominent display of recovery codes with download option
  4. Verification step before activation

Ongoing Usage:

  1. Clear indication in UI that 2FA is enabled
  2. Easy access to regenerate recovery codes
  3. Option to disable 2FA (with password + current code)
  4. Login help for users locked out

Error Handling:

  1. Clear error messages for invalid codes
  2. Instructions for users who lost authenticator access
  3. Support contact information
  4. Account recovery process documentation