โ† Research

eToro+ 2FA Architecture

System Architecture

Complete architecture for TOTP, WebAuthn/Passkeys, and backup codes in the eToro Plus ecosystem

๐Ÿ—๏ธ High-Level Architecture

graph TB subgraph Client["๐Ÿ“ฑ eToro Plus (React Native)"] A1[2FA Settings UI] A2[TOTP Code Input] A3[QR Scanner] A4[WebAuthn Prompt] A5[Backup Codes View] end subgraph Gateway["๐ŸŒ API Gateway"] B1[Auth Middleware] B2[Rate Limiter
5 req/5min] B3[JWT Validation] end subgraph Backend["โš™๏ธ 2FA Microservice (NestJS)"] C1[TOTP Controller] C2[WebAuthn Controller] C3[Backup Controller] C4[Recovery Controller] C5[2FA Guard] end subgraph Data["๐Ÿ’พ Data Layer"] D1[(PostgreSQL
user_2fa_settings
webauthn_credentials
backup_codes
audit_log)] D2[(Redis
Rate Limits
Challenge Cache)] end subgraph Security["๐Ÿ” Security"] E1[HashiCorp Vault
TOTP Secret Encryption Keys] E2[HSM
Master Key Storage] end Client -->|HTTPS/TLS 1.3| Gateway Gateway --> Backend Backend --> Data Backend --> Security C1 -.->|otplib| C1 C2 -.->|@simplewebauthn/server| C2

๐Ÿงฉ Component Diagram

graph LR subgraph Auth2FAModule TotpService[TotpService
otplib] WebAuthnService[WebAuthnService
@simplewebauthn/server] BackupCodeService[BackupCodeService] RecoveryService[RecoveryService] AuditService[AuditService] TotpController[TotpController] WebAuthnController[WebAuthnController] BackupController[BackupController] TwoFAGuard[TwoFAGuard] end TotpController --> TotpService TotpController --> AuditService WebAuthnController --> WebAuthnService WebAuthnController --> AuditService BackupController --> BackupCodeService TwoFAGuard --> TotpService TwoFAGuard --> WebAuthnService TwoFAGuard --> BackupCodeService TotpService --> VaultClient[VaultClient] VaultClient --> Vault[(Vault/HSM)]

๐Ÿ”„ Data Flow Diagrams

TOTP Setup Flow
sequenceDiagram participant U as ๐Ÿ“ฑ User (eToro Plus) participant G as ๐ŸŒ API Gateway participant S as โš™๏ธ 2FA Service participant V as ๐Ÿ” Vault participant DB as ๐Ÿ’พ PostgreSQL U->>G: POST /2fa/totp/setup (Bearer token) G->>G: Validate JWT + rate limit G->>S: Forward request S->>S: Generate TOTP secret (otplib) S->>V: Encrypt secret with AES-256-GCM V-->>S: Encrypted blob + IV S->>DB: Store encrypted secret (pending) S-->>U: { secret, qrUri: "otpauth://totp/eToro:user@email?secret=..." } Note over U: User scans QR / adds to authenticator U->>G: POST /2fa/totp/verify { code: "738204" } G->>S: Forward S->>V: Decrypt secret S->>S: Verify TOTP code (otplib.authenticator.check) S->>DB: Mark 2FA enabled, store recovery codes S-->>U: { enabled: true, backupCodes: [...] } S->>DB: INSERT audit_log (2fa_totp_enabled)
TOTP Login Verification
sequenceDiagram participant U as ๐Ÿ“ฑ User participant G as ๐ŸŒ Gateway participant A as ๐Ÿ”‘ Auth Service participant S as โš™๏ธ 2FA Service participant R as ๐Ÿ“ฆ Redis U->>G: POST /auth/login { email, password } G->>A: Validate credentials A-->>G: { requires2FA: true, tempToken } G-->>U: 200 { requires2FA: true, tempToken } U->>G: POST /2fa/totp/validate { tempToken, code } G->>S: Validate TOTP S->>R: Check rate limit (userId) alt Rate limit exceeded S-->>U: 429 Too Many Requests (lockout 5min) else S->>S: Verify code (window ยฑ1 step) S->>R: Check used codes (replay protection) alt Valid & not replayed S->>R: Store used code (TTL 90s) S-->>U: 200 { accessToken, refreshToken } else Invalid S->>R: Increment failure counter S-->>U: 401 Invalid code end end
WebAuthn Registration (Passkey Setup)
sequenceDiagram participant U as ๐Ÿ“ฑ User participant S as โš™๏ธ 2FA Service participant R as ๐Ÿ“ฆ Redis participant DB as ๐Ÿ’พ PostgreSQL U->>S: POST /2fa/webauthn/register/options S->>S: generateRegistrationOptions() S->>R: Store challenge (TTL 120s) S-->>U: { challenge, rp, user, pubKeyCredParams, ... } Note over U: Device biometric/PIN prompt U->>S: POST /2fa/webauthn/register/verify { attestation } S->>R: Retrieve & delete challenge S->>S: verifyRegistrationResponse() S->>DB: Store credential (credentialId, publicKey, counter) S-->>U: { registered: true, credentialId } S->>DB: INSERT audit_log (webauthn_registered)
WebAuthn Authentication (Passkey Login)
sequenceDiagram participant U as ๐Ÿ“ฑ User participant S as โš™๏ธ 2FA Service participant R as ๐Ÿ“ฆ Redis participant DB as ๐Ÿ’พ PostgreSQL U->>S: POST /2fa/webauthn/authenticate/options { tempToken } S->>DB: Fetch user's credentials S->>S: generateAuthenticationOptions() S->>R: Store challenge (TTL 120s) S-->>U: { challenge, allowCredentials, ... } Note over U: Biometric / PIN verification U->>S: POST /2fa/webauthn/authenticate/verify { assertion } S->>R: Retrieve & delete challenge S->>DB: Fetch credential publicKey S->>S: verifyAuthenticationResponse() S->>DB: Update credential counter (clone detection) S-->>U: { accessToken, refreshToken }
Backup Codes Flow
sequenceDiagram participant U as ๐Ÿ“ฑ User participant S as โš™๏ธ 2FA Service participant DB as ๐Ÿ’พ PostgreSQL Note over U,S: Generation (during 2FA setup) S->>S: Generate 10 codes (crypto.randomBytes) S->>S: Hash each code (bcrypt, cost 10) S->>DB: Store hashed codes S-->>U: { codes: ["XXXX-XXXX", ...] } (shown once) Note over U,S: Usage (during login) U->>S: POST /2fa/backup-codes/verify { code: "ABCD-1234" } S->>DB: Fetch unused hashed codes S->>S: bcrypt.compare against each alt Match found S->>DB: Mark code as used + timestamp S-->>U: 200 { accessToken } + warning: N codes remaining else No match S-->>U: 401 Invalid backup code end
SMS Fallback โ†’ TOTP/Passkey Migration
sequenceDiagram participant U as ๐Ÿ“ฑ User participant S as โš™๏ธ 2FA Service participant SMS as ๐Ÿ“จ SMS Provider Note over U,SMS: User currently has SMS-based 2FA U->>S: POST /2fa/totp/setup S-->>U: { secret, qrUri } + prompt to upgrade U->>U: Scan QR, enter code U->>S: POST /2fa/totp/verify { code } S->>S: Verify TOTP works S->>S: Disable SMS fallback S->>SMS: Remove phone from SMS 2FA pool S-->>U: { method: "totp", smsMigrated: true } S->>S: Log audit: sms_to_totp_migration

๐Ÿข Infrastructure & Secret Storage

๐Ÿ” Secret Management

SecretStorageEncryption
TOTP SecretsPostgreSQL (encrypted blob)AES-256-GCM via Vault Transit
Encryption Master KeyHSM (FIPS 140-2 L3)Hardware-protected
WebAuthn Private KeysClient device (Secure Enclave)Device-managed
WebAuthn Public KeysPostgreSQLPlaintext (public)
Backup CodesPostgreSQL (hashed)bcrypt (cost 10)
Challenge NoncesRedis (TTL 120s)N/A (ephemeral)

โšก Rate Limiting & Caching

ResourceLimitWindow
TOTP validation5 attempts5 minutes
WebAuthn auth5 attempts5 minutes
Backup code verify3 attempts15 minutes
TOTP setup3 requests1 hour
Account recovery1 request24 hours

Implemented via @nestjs/throttler with Redis backing store. Lockout triggers alert to security team after 3 consecutive lockouts.

๐Ÿ“‹ Audit Logging

Every 2FA event is logged to 2fa_audit_log for compliance (FCA/CySEC). Logs are immutable (append-only) and retained for 7 years.

{
  "id": "uuid",
  "userId": "12345",
  "action": "totp_validate_success",
  "ipAddress": "203.0.113.42",
  "userAgent": "eToro Plus/3.2.1 (iOS 17.3)",
  "deviceFingerprint": "sha256:abc...",
  "timestamp": "2026-02-22T14:22:00Z",
  "metadata": { "method": "totp", "attemptsInWindow": 1 }
}

๐Ÿ›ก๏ธ Security Considerations

๐Ÿ” Replay Protection

  • TOTP codes stored in Redis with TTL = time step (30s) ร— 3
  • Same code cannot be used twice within the window
  • WebAuthn challenges are single-use (deleted after verification)
  • Credential counter incremented on each WebAuthn auth โ€” clone detection

โฑ๏ธ Timing Attack Mitigation

  • TOTP: crypto.timingSafeEqual() for code comparison
  • Backup codes: bcrypt comparison (constant-time by design)
  • All responses return in consistent time (ยฑ50ms jitter added on failure)
  • Rate limiting applied before validation to prevent enumeration

๐Ÿšซ Brute Force Protection

  • 5 failed attempts โ†’ 5-minute lockout (exponential: 5, 15, 60 min)
  • 10 failed attempts in 1 hour โ†’ account flagged + email alert
  • IP-based + user-based dual rate limiting
  • CAPTCHA challenge after 3rd failed attempt

๐Ÿ”„ Key Rotation

  • Vault transit keys rotated quarterly (versioned โ€” old data still decryptable)
  • TOTP re-encryption job runs monthly with latest key version
  • WebAuthn RP ID and origin validated on every request
  • JWT signing keys rotated weekly (RS256, 2048-bit)

๐Ÿ”‘ Passkey (WebAuthn/FIDO2) Deep Dive

Passkey vs TOTP Comparison

CriteriaTOTP AuthenticatorPasskey (WebAuthn)
Phishing ResistanceNo โ€” User can be tricked into entering code on fake siteYes โ€” Origin-bound; cryptographically tied to RP ID
User ExperienceOpen app โ†’ read code โ†’ type 6 digits (10-15s)Tap button โ†’ biometric (Face ID/fingerprint) โ†’ done (2-3s)
Works OfflineYesYes (platform authenticator)
Cross-DeviceYes โ€” Any authenticator appYes โ€” Synced passkeys + hybrid transport (QR)
Secret ExposureShared secret on server + clientPrivate key never leaves device/enclave
Replay ProtectionTime window (~90s vulnerability)Challenge-response; each auth unique
Man-in-the-MiddleVulnerableProtected โ€” TLS channel binding
Device SupportUniversal (any TOTP app)iOS 16+, Android 9+, Windows 10+, macOS Ventura+
Passwordless CapableNoYes โ€” Discoverable credentials
Cost per Auth$0$0
Implementation ComplexityLow (otplib)Medium (@simplewebauthn)

Passkey Sync Architecture

Passkeys are synced WebAuthn credentials โ€” the private key is securely replicated across a user's devices via platform credential managers. This eliminates the "lost device = locked out" problem.

graph TB subgraph Apple["๐ŸŽ Apple Ecosystem"] IP[iPhone โ€” Secure Enclave] -->|iCloud Keychain
E2E Encrypted| IC[(iCloud)] IC --> MB[MacBook โ€” Secure Enclave] IC --> iPad[iPad โ€” Secure Enclave] end subgraph Google["๐Ÿค– Google Ecosystem"] Pixel[Android Phone] -->|Google Password Manager
E2E Encrypted| GP[(Google Cloud)] GP --> Chrome[Chrome Desktop] GP --> Tablet[Android Tablet] end subgraph Microsoft["๐ŸชŸ Windows Ecosystem"] WH[Windows Hello] -->|Microsoft Account
TPM-backed| MA[(Microsoft Cloud)] MA --> WH2[Windows Device 2] end subgraph eToro["๐Ÿฆ eToro Server"] DB[(Stores only:
Public Key +
Credential ID +
Metadata)] end IP -.->|WebAuthn Assertion| DB Pixel -.->|WebAuthn Assertion| DB WH -.->|WebAuthn Assertion| DB style DB fill:#0a0a2e,stroke:#13C636

Key insight: eToro never sees the private key. Only the public key is stored server-side. Sync is handled entirely by the platform (Apple/Google/Microsoft) using end-to-end encryption.

Cross-Device Authentication (Hybrid Transport)

When a user needs to authenticate on a device without a registered passkey (e.g., logging in on a friend's laptop), the hybrid transport protocol enables using their phone as an authenticator via QR code + Bluetooth proximity.

sequenceDiagram participant L as ๐Ÿ’ป Laptop (no passkey) participant S as โš™๏ธ eToro Server participant P as ๐Ÿ“ฑ User's Phone (has passkey) L->>S: POST /passkey/authenticate/options
{allowCredentials: []} S-->>L: {challenge, allowCredentials: [...]} L->>L: Browser shows QR code
(contains BLE advert + tunnel info) Note over L,P: User scans QR with phone camera P->>P: Phone detects QR โ†’ connects via
BLE proximity + CTAP2 tunnel P->>P: Biometric prompt (Face ID / fingerprint) P->>L: Signed assertion via encrypted tunnel L->>S: POST /passkey/authenticate/verify S-->>L: {accessToken, refreshToken} โœ…
Discoverable Credentials โ€” Passwordless Flow

With discoverable credentials (resident keys), the credential includes the user handle โ€” enabling login without entering a username. The browser's autofill UI suggests matching passkeys for the site.

sequenceDiagram participant U as ๐Ÿ‘ค User participant B as ๐ŸŒ Browser (Conditional UI) participant S as โš™๏ธ eToro Server U->>B: Navigate to etoro.com/login B->>S: POST /passkey/authenticate/options
{mediation: "conditional"} S-->>B: {challenge} (no allowCredentials โ€” discoverable) B->>B: Autofill UI shows available passkeys
"Sign in as yoni@etoro.com ๐Ÿ”‘" U->>B: Taps passkey suggestion B->>B: Biometric verification (Face ID) B->>S: POST /passkey/authenticate/verify
{assertion with userHandle} S->>S: Look up user by userHandle S->>S: Verify assertion signature S-->>B: {accessToken} โ€” Logged in! โœ… Note over U,S: No username. No password. No OTP. Just biometrics.
Passkey Registration Sequence (Detailed)
sequenceDiagram participant U as ๐Ÿ“ฑ User participant A as ๐Ÿ“ฒ eToro App participant S as โš™๏ธ 2FA Service participant R as ๐Ÿ“ฆ Redis participant DB as ๐Ÿ’พ PostgreSQL U->>A: Tap "Add Passkey" in Security settings A->>S: POST /passkey/register/options S->>S: generateRegistrationOptions({
rpName: "eToro", rpID: "etoro.com",
userName: user.email,
attestationType: "none",
residentKey: "required",
userVerification: "preferred"
}) S->>R: Store challenge (key=userId, TTL=120s) S-->>A: {challenge, rp, user, pubKeyCredParams, excludeCredentials} A->>A: startRegistration(options) Note over U,A: OS biometric prompt:
"Create a passkey for etoro.com?"
Face ID / Touch ID / PIN U->>A: Authenticates with biometric A->>S: POST /passkey/register/verify {attestationResponse} S->>R: Retrieve + delete challenge S->>S: verifyRegistrationResponse({
expectedChallenge, expectedOrigin,
expectedRPID: "etoro.com"
}) S->>DB: INSERT webauthn_credentials {
credential_id, public_key, counter: 0,
transports, device_type, backed_up,
aaguid, device_name
} S->>DB: UPDATE user_2fa_settings SET webauthn_enabled=true S-->>A: {registered: true, credentialId, deviceName} A->>U: โœ… "Passkey created for iPhone 15 Pro"
Passkey Login with Biometric Sequence
sequenceDiagram participant U as ๐Ÿ‘ค User participant A as ๐Ÿ“ฒ eToro App participant S as โš™๏ธ 2FA Service participant R as ๐Ÿ“ฆ Redis participant DB as ๐Ÿ’พ PostgreSQL U->>A: Enter email + password A->>S: POST /auth/login S-->>A: {requires2FA: true, tempToken, methods: ["passkey","totp"]} A->>A: Show "Use Passkey" button (preferred) U->>A: Tap "Sign in with Passkey" A->>S: POST /passkey/authenticate/options {tempToken} S->>DB: Fetch user's credentials S->>S: generateAuthenticationOptions({
allowCredentials: user.credentials,
userVerification: "preferred"
}) S->>R: Store challenge (TTL=120s) S-->>A: {challenge, allowCredentials, timeout} A->>A: startAuthentication(options) Note over U,A: Biometric prompt:
"Sign in to etoro.com"
Face ID โ†’ โœ… U->>A: Biometric verified A->>S: POST /passkey/authenticate/verify {assertion} S->>R: Retrieve + delete challenge S->>DB: Fetch credential public key S->>S: verifyAuthenticationResponse() S->>DB: UPDATE counter, last_used_at S-->>A: {accessToken, refreshToken} โœ… A->>U: Welcome back! ๐ŸŽ‰

Platform Support Matrix

PlatformPasskey SupportSyncCross-Device (Hybrid)Conditional UINotes
iOS 16+ / Safariโœ… FulliCloud Keychainโœ…โœ…Best passkey support; 97% of iPhones eligible
Android 9+ / Chromeโœ… FullGoogle Password Managerโœ…โœ…Requires Google Play Services
macOS Ventura+ / Safariโœ… FulliCloud Keychainโœ…โœ…Touch ID or Apple Watch
macOS / Chrome 118+โœ… FulliCloud Keychain or Chromeโœ…โœ…Chrome profiles synced via Google
Windows 10+ / Chrome/Edgeโœ… FullWindows Hello (local)โœ…โœ…TPM 2.0 required; sync coming via Microsoft
Windows / Firefox 122+โœ…OS-levelโœ…PartialDelegates to Windows Hello
Linux / ChromePartialNo syncโœ…PartialUSB security keys + phone-as-authenticator
Security Keys (YubiKey)โœ…No (single device)N/AN/AFIDO2 USB/NFC; enterprise users

๐Ÿš€ 1-Week Build & Deploy Roadmap

DayFocusDeliverablesOwner
Day 1 (Mon)Project Setup + Core TOTPNestJS scaffold, DB schemas (all 5 tables), TypeORM entities, TOTP service (otplib), setup/verify/validate/disable endpoints workingBackend
Day 2 (Tue)WebAuthn/Passkey + All EndpointsWebAuthn service (@simplewebauthn/server), register options/verify, authenticate options/verify, list/delete/rename credentials, backup codes generate/verify, status, recovery โ€” all 19 endpoints doneBackend
Day 3 (Wed)Client TOTP FlowsTOTP setup wizard (QR display, code input, backup codes screen), TOTP login verification screen, CodeInput component, QRCodeDisplay componentFrontend
Day 4 (Thu)Client Passkey FlowsPasskey registration flow, passkey login (biometric), manage passkeys (list/rename/delete), conditional UI (autofill), cross-device QR flow, passkey nudge bottom sheetFrontend
Day 5 (Fri)Backup & RecoveryBackup code input screen, recovery flow, fallback chain (passkey โ†’ TOTP โ†’ backup โ†’ recovery), disable 2FA flow, all edge case handlingFull-stack
Day 6 (Sat)Testing & QAE2E tests (TOTP, WebAuthn, backup codes), integration tests, rate limiting verification, security audit (replay protection, timing attacks, brute force), Swagger docs finalizedQA + Security
Day 7 (Sun)Deploy & Go-Live PrepStaging deployment, load testing, security team sign-off, monitoring/alerting (Datadog), runbook for on-call, feature flag configuration, go-live checklist completeDevOps + All

AI-speed timeline: Full 2FA system (TOTP + Passkeys + backup codes) built, tested, and deployed in 1 week. SMS migration nudges ship on Day 7 go-live.

๐Ÿ“ฆ Deployment Architecture

graph TB subgraph K8s["Kubernetes Cluster"] subgraph Pods["2FA Service Pods (3 replicas)"] P1[Pod 1] P2[Pod 2] P3[Pod 3] end Ingress[Ingress Controller
TLS Termination] --> Pods HPA[HPA
CPU 70% / Memory 80%] end LB[CloudFlare / AWS ALB] --> Ingress Pods --> PG[(PostgreSQL
Primary + 2 Replicas)] Pods --> Redis[(Redis Cluster
3 nodes)] Pods --> Vault[(HashiCorp Vault
HA mode)] Pods --> Datadog[Datadog
Metrics + Alerts]
โ† Back to Research ยท Server SpecKit โ†’ ยท Client SpecKit โ†’ ยท Swagger Docs โ†’