Design (LLD) Google Authenticator - Machine Coding
Features Required:
QR Code Generation: Ability to generate QR codes for two-factor authentication.
Token Generation: Generate time-based tokens for authentication.
Token Verification: Verify the correctness of the entered token.
Secret Key Storage: Securely store and retrieve secret keys associated with users.
User Registration: Register new users for two-factor authentication.
User Management: Manage users, including adding, updating, and removing them.
Algorithm Flexibility: Support for different hashing algorithms.
Time Synchronization: Handle time synchronization issues.
Design Patterns Involved or Used:
Singleton Pattern: For the
GoogleAuthenticatorinstance, ensuring a single point of access to the authentication system.Factory Method Pattern: For creating instances of authentication tokens, allowing flexibility for different token types.
Strategy Pattern: For supporting different hashing algorithms and making them interchangeable.
Facade Pattern: Simplifying the complexity of the authentication system by providing a higher-level interface.
Observer Pattern: For notifying users or systems about events like successful or failed authentication attempts.
Command Pattern: For encapsulating token generation and verification requests as objects.
Data Access Object (DAO) Pattern: For handling data access and storage, especially for user-related data.
Diagram


Code
import java.security.Key;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
// Command Interface
interface TokenCommand {
void execute();
}
// Token Generation Command
class GenerateTokenCommand implements TokenCommand {
private TokenFactory tokenFactory;
private String secretKey;
private long currentTimeMillis;
public GenerateTokenCommand(TokenFactory tokenFactory, String secretKey, long currentTimeMillis) {
this.tokenFactory = tokenFactory;
this.secretKey = secretKey;
this.currentTimeMillis = currentTimeMillis;
}
public void execute() {
// Execute token generation
String generatedToken = tokenFactory.generateToken(secretKey, currentTimeMillis);
System.out.println("Generated Token: " + generatedToken);
// Additional logic, if needed
}
}
// Token Verification Command
class VerifyTokenCommand implements TokenCommand {
private TokenVerifier tokenVerifier;
private String enteredCode;
private String generatedCode;
public VerifyTokenCommand(TokenVerifier tokenVerifier, String enteredCode, String generatedCode) {
this.tokenVerifier = tokenVerifier;
this.enteredCode = enteredCode;
this.generatedCode = generatedCode;
}
public void execute() {
// Execute token verification
boolean isVerified = tokenVerifier.verifyToken(enteredCode, generatedCode);
System.out.println("Token Verification Result: " + isVerified);
// Additional logic, if needed
}
}
// Token Operation Invoker
class TokenOperationInvoker {
private TokenCommand tokenCommand;
public void setCommand(TokenCommand tokenCommand) {
this.tokenCommand = tokenCommand;
}
public void executeOperation() {
// Execute the assigned command
tokenCommand.execute();
}
}
// Singleton Pattern
class GoogleAuthenticator {
private static GoogleAuthenticator instance;
private TokenFactory tokenFactory;
private TokenVerifier tokenVerifier;
private UserDao userDao;
private TokenOperationInvoker tokenOperationInvoker;
private GoogleAuthenticator() {
this.tokenFactory = new DefaultTokenFactory();
this.tokenVerifier = new DefaultTokenVerifier();
this.userDao = new InMemoryUserDao();
this.tokenOperationInvoker = new TokenOperationInvoker();
}
public static GoogleAuthenticator getInstance() {
if (instance == null) {
instance = new GoogleAuthenticator();
}
return instance;
}
// Other methods for QR code generation, user registration, etc.
public void generateAndExecuteTokenGenerationCommand(String userId, long currentTimeMillis) {
User user = userDao.getUser(userId);
if (user != null) {
String secretKey = user.getSecretKey();
// Create the token generation command
TokenCommand generateTokenCommand = new GenerateTokenCommand(tokenFactory, secretKey, currentTimeMillis);
// Set the command
tokenOperationInvoker.setCommand(generateTokenCommand);
// Execute the command
tokenOperationInvoker.executeOperation();
}
}
public void generateAndExecuteTokenVerificationCommand(String userId, String enteredCode, String generatedCode) {
User user = userDao.getUser(userId);
if (user != null) {
// Create the token verification command
TokenCommand verifyTokenCommand = new VerifyTokenCommand(tokenVerifier, enteredCode, generatedCode);
// Set the command
tokenOperationInvoker.setCommand(verifyTokenCommand);
// Execute the command
tokenOperationInvoker.executeOperation();
}
}
// Other methods...
}
// Factory Method Pattern
interface TokenFactory {
String generateToken(String secretKey, long currentTimeMillis);
}
class DefaultTokenFactory implements TokenFactory {
@Override
public String generateToken(String secretKey, long currentTimeMillis) {
// Implementation details...
return "generated_token";
}
}
// Strategy Pattern
interface TokenVerifier {
boolean verifyToken(String enteredCode, String generatedCode);
}
class DefaultTokenVerifier implements TokenVerifier {
@Override
public boolean verifyToken(String enteredCode, String generatedCode) {
// Implementation details...
return enteredCode.equals(generatedCode);
}
}
// Data Access Object (DAO) Pattern
interface UserDao {
void addUser(User user);
User getUser(String userId);
void updateUser(User user);
void removeUser(String userId);
}
class User {
private String userId;
private String name;
private String secretKey;
public User(String userId, String name, String secretKey) {
this.userId = userId;
this.name = name;
this.secretKey = secretKey;
}
public String getUserId() {
return userId;
}
public String getSecretKey() {
return secretKey;
}
}
class InMemoryUserDao implements UserDao {
private Map<String, User> users;
public InMemoryUserDao() {
this.users = new HashMap<>();
}
@Override
public void addUser(User user) {
users.put(user.getUserId(), user);
}
@Override
public User getUser(String userId) {
return users.get(userId);
}
@Override
public void updateUser(User user) {
users.put(user.getUserId(), user);
}
@Override
public void removeUser(String userId) {
users.remove(userId);
}
}
public class GoogleAuthenticatorApp {
public static void main(String[] args) {
GoogleAuthenticator authenticator = GoogleAuthenticator.getInstance();
User user = new User("123", "John Doe", "secretKey123");
authenticator.generateAndExecuteTokenGenerationCommand(user.getUserId(), System.currentTimeMillis());
// Simulating user entering the code
String enteredCode = "generated_token"; // Replace with the actual user input
authenticator.generateAndExecuteTokenVerificationCommand(user.getUserId(), enteredCode, "generated_token");
}
}
This is a simplified implementation showcasing some aspects of the design. The actual implementation may require additional considerations and refinements based on specific use cases and security requirements.
Issues in Above Design
1️⃣ Singleton God Object (LLD + Concurrency)
Issue
GoogleAuthenticator is:
Singleton
Holds too many responsibilities
Manages commands, DAO, token logic, and execution
LLD Problem
Violates Single Responsibility Principle
Hard to test, extend, or replace components
Tight coupling between authentication flow and infrastructure
Concurrency Problem
All threads share the same instance
Shared mutable fields lead to race conditions
Impact
Incorrect command execution
Authentication logic interference across users
Difficult horizontal scaling
