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