Understanding Multi-Factor Authentication(MFA)

November 22, 20255 min read
Multi-Factor Authentication (MFA) adds an extra layer of security to your accounts by requiring multiple forms of verification. Instead of just a password, you need something else to prove you're really you.

Authentication factors fall into three categories:

  1. Something You Know - Passwords, PINs, security questions
  2. Something You Have - Phone, security token, smart card
  3. Something You Are - Fingerprint, face recognition, voice

How Time-Based OTP (TOTP) Works

The most common MFA method uses Time-Based One-Time Passwords (TOTP). Here's how it works:

Python Implementation

Let's implement a simple MFA system using TOTP:

Installation

pip install pyotp qrcode[pil]

Basic MFA Implementation

import pyotp
import qrcode
from io import BytesIO
import base64

class MFASystem:
    """Simple Multi-Factor Authentication System"""
    
    def __init__(self):
        self.users = {}
    
    def register_user(self, username, password):
        """Register a new user and generate MFA secret"""
        # Generate a random secret key
        secret = pyotp.random_base32()
        
        # Store user credentials and secret
        self.users[username] = {
            'password': password,  # In production, hash this!
            'secret': secret
        }
        
        return secret
    
    def get_qr_code(self, username, secret):
        """Generate QR code for authenticator app"""
        # Create provisioning URI
        uri = pyotp.totp.TOTP(secret).provisioning_uri(
            name=username,
            issuer_name="MyApp"
        )
        
        # Generate QR code
        qr = qrcode.QRCode(version=1, box_size=10, border=5)
        qr.add_data(uri)
        qr.make(fit=True)
        
        img = qr.make_image(fill_color="black", back_color="white")
        
        # Convert to base64 for display
        buffer = BytesIO()
        img.save(buffer, format='PNG')
        img_str = base64.b64encode(buffer.getvalue()).decode()
        
        return img_str
    
    def verify_credentials(self, username, password):
        """Verify username and password (Factor 1)"""
        if username not in self.users:
            return False
        return self.users[username]['password'] == password
    
    def verify_otp(self, username, otp_code):
        """Verify the OTP code (Factor 2)"""
        if username not in self.users:
            return False
        
        secret = self.users[username]['secret']
        totp = pyotp.TOTP(secret)
        
        # Verify OTP with 30-second window
        return totp.verify(otp_code, valid_window=1)
    
    def login(self, username, password, otp_code):
        """Complete login with both factors"""
        # Factor 1: Password
        if not self.verify_credentials(username, password):
            return False, "Invalid username or password"
        
        # Factor 2: OTP
        if not self.verify_otp(username, otp_code):
            return False, "Invalid OTP code"
        
        return True, "Login successful"


# Example Usage
def main():
    mfa = MFASystem()
    
    # Step 1: Register a new user
    username = "alice"
    password = "SecurePassword123"
    
    print("=== User Registration ===")
    secret = mfa.register_user(username, password)
    print(f"User '{username}' registered successfully!")
    print(f"Secret Key: {secret}")
    print("Scan this QR code with your authenticator app:")
    
    # Generate QR code (in production, display this as an image)
    qr_code = mfa.get_qr_code(username, secret)
    print(f"QR Code (base64): {qr_code[:50]}...")
    
    # Step 2: Generate OTP manually (simulating authenticator app)
    print("\n=== Login Process ===")
    totp = pyotp.TOTP(secret)
    current_otp = totp.now()
    print(f"Current OTP from authenticator: {current_otp}")
    
    # Step 3: Login with both factors
    success, message = mfa.login(username, password, current_otp)
    print(f"\nLogin attempt: {message}")
    
    # Test with wrong OTP
    print("\n=== Testing Wrong OTP ===")
    success, message = mfa.login(username, password, "000000")
    print(f"Login attempt: {message}")


if __name__ == "__main__":
    main()

Output Example

=== User Registration ===
User 'alice' registered successfully!
Secret Key: JBSWY3DPEHPK3PXP
Scan this QR code with your authenticator app:
QR Code (base64): iVBORw0KGgoAAAANSUhEUgAAAXIAAAFyAQAAAADAX...

=== Login Process ===
Current OTP from authenticator: 847291

Login attempt: Login successful

=== Testing Wrong OTP ===
Login attempt: Invalid OTP code

How the Algorithm Works

The TOTP algorithm generates codes using:

TOTP = HMAC-SHA1(Secret, Time Counter)

Key Points:

  • The time counter is current_unix_time / 30 (changes every 30 seconds)
  • Both server and authenticator use the same algorithm
  • They generate the same code because they share the secret key
  • Codes expire quickly (30 seconds) for security

Security Best Practices

  1. Store Secrets Securely: Never store MFA secrets in plain text
  2. Hash Passwords: Use bcrypt or similar for password storage
  3. Use HTTPS: Always transmit credentials over secure connections
  4. Rate Limiting: Limit OTP verification attempts
  5. Backup Codes: Provide recovery codes in case of device loss
import bcrypt

def hash_password(password):
    """Securely hash a password"""
    salt = bcrypt.gensalt()
    return bcrypt.hashpw(password.encode(), salt)

def verify_password(password, hashed):
    """Verify a password against its hash"""
    return bcrypt.checkpw(password.encode(), hashed)

Common MFA Methods

Conclusion

MFA significantly improves security by requiring multiple verification methods. Even if someone steals your password, they can't access your account without the second factor. The Python implementation above provides a foundation for building MFA into your applications.

Remember: MFA is not unbreakable, but it makes attacks exponentially harder. Combined with other security practices, it's one of the most effective ways to protect user accounts.