โ† Research

Server SpecKit

Server-Side SpecKit

Complete backend specification for the eToro 2FA NestJS microservice

๐Ÿ“ฆ NestJS Module Structure

src/
โ”œโ”€โ”€ auth-2fa/
โ”‚   โ”œโ”€โ”€ auth-2fa.module.ts          # Auth2FAModule
โ”‚   โ”œโ”€โ”€ controllers/
โ”‚   โ”‚   โ”œโ”€โ”€ totp.controller.ts      # TOTP endpoints
โ”‚   โ”‚   โ”œโ”€โ”€ webauthn.controller.ts  # WebAuthn endpoints
โ”‚   โ”‚   โ”œโ”€โ”€ backup.controller.ts    # Backup codes
โ”‚   โ”‚   โ””โ”€โ”€ recovery.controller.ts  # Account recovery
โ”‚   โ”œโ”€โ”€ services/
โ”‚   โ”‚   โ”œโ”€โ”€ totp.service.ts         # TOTP logic (otplib)
โ”‚   โ”‚   โ”œโ”€โ”€ webauthn.service.ts     # WebAuthn (@simplewebauthn/server)
โ”‚   โ”‚   โ”œโ”€โ”€ backup-code.service.ts  # Backup code generation/verification
โ”‚   โ”‚   โ”œโ”€โ”€ recovery.service.ts     # Account recovery flow
โ”‚   โ”‚   โ”œโ”€โ”€ vault.service.ts        # Vault/HSM encryption client
โ”‚   โ”‚   โ””โ”€โ”€ audit.service.ts        # Audit logging
โ”‚   โ”œโ”€โ”€ guards/
โ”‚   โ”‚   โ”œโ”€โ”€ two-fa.guard.ts         # Require 2FA on protected routes
โ”‚   โ”‚   โ””โ”€โ”€ two-fa-setup.guard.ts   # Ensure user has active session
โ”‚   โ”œโ”€โ”€ decorators/
โ”‚   โ”‚   โ””โ”€โ”€ require-2fa.decorator.ts
โ”‚   โ”œโ”€โ”€ dto/
โ”‚   โ”‚   โ”œโ”€โ”€ totp-setup.dto.ts
โ”‚   โ”‚   โ”œโ”€โ”€ totp-verify.dto.ts
โ”‚   โ”‚   โ”œโ”€โ”€ webauthn-register.dto.ts
โ”‚   โ”‚   โ”œโ”€โ”€ webauthn-auth.dto.ts
โ”‚   โ”‚   โ””โ”€โ”€ backup-verify.dto.ts
โ”‚   โ”œโ”€โ”€ entities/
โ”‚   โ”‚   โ”œโ”€โ”€ user-2fa-settings.entity.ts
โ”‚   โ”‚   โ”œโ”€โ”€ totp-secret.entity.ts
โ”‚   โ”‚   โ”œโ”€โ”€ webauthn-credential.entity.ts
โ”‚   โ”‚   โ”œโ”€โ”€ backup-code.entity.ts
โ”‚   โ”‚   โ””โ”€โ”€ audit-log.entity.ts
โ”‚   โ””โ”€โ”€ constants/
โ”‚       โ””โ”€โ”€ 2fa.constants.ts
โ”œโ”€โ”€ app.module.ts
โ””โ”€โ”€ main.ts

Module Registration

// auth-2fa.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([
      User2FASettings, TotpSecret,
      WebAuthnCredential, BackupCode, AuditLog
    ]),
    ThrottlerModule.forRoot({ ttl: 300, limit: 5 }),
    VaultModule,
    RedisModule,
  ],
  controllers: [
    TotpController, WebAuthnController,
    BackupController, RecoveryController
  ],
  providers: [
    TotpService, WebAuthnService, BackupCodeService,
    RecoveryService, VaultService, AuditService,
    TwoFAGuard, TwoFASetupGuard
  ],
  exports: [TwoFAGuard, TotpService, WebAuthnService]
})
export class Auth2FAModule {}

๐Ÿ—„๏ธ Database Schema

user_2fa_settings
ColumnTypeConstraintsDescription
idUUIDPK, DEFAULT gen_random_uuid()Primary key
user_idBIGINTUNIQUE, NOT NULL, FK โ†’ users.ideToro user ID
is_enabledBOOLEANDEFAULT false2FA active?
primary_methodENUM('totp','webauthn','sms')NULLABLEPrimary 2FA method
totp_enabledBOOLEANDEFAULT falseTOTP active
webauthn_enabledBOOLEANDEFAULT falseWebAuthn active
backup_codes_remainingSMALLINTDEFAULT 0Unused backup codes count
lockout_untilTIMESTAMPTZNULLABLELockout expiry
failed_attemptsSMALLINTDEFAULT 0Current window failures
created_atTIMESTAMPTZDEFAULT NOW()
updated_atTIMESTAMPTZDEFAULT NOW()
totp_secrets
ColumnTypeConstraintsDescription
idUUIDPK
user_idBIGINTUNIQUE, FK
encrypted_secretBYTEANOT NULLAES-256-GCM encrypted TOTP secret
ivBYTEA(16)NOT NULLInitialization vector
vault_key_versionSMALLINTNOT NULLVault transit key version used
algorithmVARCHAR(10)DEFAULT 'sha1'HMAC algorithm
digitsSMALLINTDEFAULT 6
periodSMALLINTDEFAULT 30Seconds per step
verifiedBOOLEANDEFAULT falseUser confirmed code works
created_atTIMESTAMPTZDEFAULT NOW()
webauthn_credentials (Passkeys)
ColumnTypeConstraintsDescription
idUUIDPK
user_idBIGINTFK, INDEX
credential_idBYTEAUNIQUE, NOT NULLWebAuthn credential ID (base64url)
public_keyBYTEANOT NULLCOSE public key (CBOR-encoded)
sign_countBIGINTDEFAULT 0, NOT NULLSignature counter (clone detection)
transportsVARCHAR[]['internal','hybrid','ble','usb','nfc']
device_nameVARCHAR(100)User-friendly name (e.g. "iPhone 15 Pro")
last_used_atTIMESTAMPTZLast successful authentication
aaguidUUIDAuthenticator attestation GUID (identifies make/model)
backup_eligibleBOOLEANDEFAULT falseBE flag โ€” credential CAN be backed up (multi-device)
backup_stateBOOLEANDEFAULT falseBS flag โ€” credential IS currently backed up/synced
device_typeVARCHAR(32)'singleDevice' or 'multiDevice' (derived from BE)
credential_typeVARCHAR(20)DEFAULT 'passkey''passkey' (discoverable) or 'security-key'
created_atTIMESTAMPTZDEFAULT NOW()
-- Indexes
CREATE INDEX idx_webauthn_user ON webauthn_credentials(user_id);
CREATE UNIQUE INDEX idx_webauthn_cred ON webauthn_credentials(credential_id);
CREATE INDEX idx_webauthn_aaguid ON webauthn_credentials(aaguid);

-- Max 10 passkeys per user (enforced at app level, checked on insert)
-- Soft constraint: credential_type = 'passkey' โ†’ backup_eligible = true typically
backup_codes
ColumnTypeConstraintsDescription
idUUIDPK
user_idBIGINTFK, INDEX
code_hashVARCHAR(60)NOT NULLbcrypt hash of backup code
usedBOOLEANDEFAULT false
used_atTIMESTAMPTZ
created_atTIMESTAMPTZDEFAULT NOW()
2fa_audit_log
ColumnTypeConstraintsDescription
idUUIDPK
user_idBIGINTINDEX
actionVARCHAR(50)NOT NULL, INDEXe.g. totp_setup, webauthn_auth_success
ip_addressINET
user_agentTEXT
device_fingerprintVARCHAR(64)SHA-256 device ID
metadataJSONBExtra event data
created_atTIMESTAMPTZDEFAULT NOW(), INDEXImmutable, append-only

Partition by month. Retention: 7 years (regulatory). Table is INSERT-only (no UPDATE/DELETE grants).

๐Ÿ”Œ API Endpoints

MethodEndpointDescriptionAuthRate Limit
POST/api/v1/2fa/totp/setupGenerate TOTP secret + QR URIBearer3/hour
POST/api/v1/2fa/totp/verifyVerify code & enable TOTPBearer5/5min
POST/api/v1/2fa/totp/validateValidate code during loginTemp token5/5min
DELETE/api/v1/2fa/totpDisable TOTP (requires current code)Bearer + 2FA2/hour
POST/api/v1/2fa/passkey/register/optionsGet passkey registration options (resident key)Bearer3/hour
POST/api/v1/2fa/passkey/register/verifyVerify & store passkey credentialBearer5/5min
POST/api/v1/2fa/passkey/authenticate/optionsGet auth options (discoverable or allow-list)Temp token / None5/5min
POST/api/v1/2fa/passkey/authenticate/verifyVerify passkey assertionTemp token / None5/5min
GET/api/v1/2fa/passkey/credentialsList user's registered passkeysBearer30/min
DELETE/api/v1/2fa/passkey/credentials/:idRemove a passkeyBearer + 2FA5/hour
POST/api/v1/2fa/passkey/renameRename a passkey device nameBearer10/hour
POST/api/v1/2fa/backup-codes/generateGenerate 10 backup codesBearer + 2FA1/day
POST/api/v1/2fa/backup-codes/verifyUse a backup codeTemp token3/15min
GET/api/v1/2fa/statusGet user's 2FA configurationBearer30/min
POST/api/v1/2fa/recoveryInitiate account recoveryEmail token1/24hr
POST /api/v1/2fa/totp/setup โ€” Generate TOTP Secret

Generates a new TOTP secret, encrypts via Vault, stores pending, and returns QR URI.

Request

Headers:
  Authorization: Bearer <accessToken>

Body: (empty)

Response 200

{
  "secret": "JBSWY3DPEHPK3PXP",
  "qrUri": "otpauth://totp/eToro:user@email.com?secret=JBSWY3DPEHPK3PXP&issuer=eToro&algorithm=SHA1&digits=6&period=30",
  "algorithm": "SHA1",
  "digits": 6,
  "period": 30
}

Error Responses

StatusCodeDescription
409TOTP_ALREADY_ENABLEDUser already has TOTP active
429RATE_LIMIT_EXCEEDEDToo many setup attempts
POST /api/v1/2fa/totp/verify โ€” Verify & Enable TOTP

Request

{
  "code": "738204"
}

Response 200

{
  "enabled": true,
  "method": "totp",
  "backupCodes": [
    "A1B2-C3D4", "E5F6-G7H8",
    "I9J0-K1L2", "M3N4-O5P6",
    "Q7R8-S9T0", "U1V2-W3X4",
    "Y5Z6-A7B8", "C9D0-E1F2",
    "G3H4-I5J6", "K7L8-M9N0"
  ]
}
POST /api/v1/2fa/totp/validate โ€” Login Validation

Request

Headers:
  X-Temp-Token: <tempToken>

{
  "code": "738204"
}

Response 200

{
  "accessToken": "eyJhbG...",
  "refreshToken": "eyJhbG...",
  "expiresIn": 3600
}

Error Responses

StatusCodeDescription
401INVALID_TOTP_CODECode doesn't match
401CODE_ALREADY_USEDReplay detected
423ACCOUNT_LOCKEDToo many failed attempts
429RATE_LIMIT_EXCEEDEDThrottled
DELETE /api/v1/2fa/totp โ€” Disable TOTP

Request

{
  "code": "738204"
}

Response 200

{
  "disabled": true,
  "method": null
}
POST /api/v1/2fa/passkey/register/options โ€” Get Passkey Registration Options

Returns WebAuthn registration options with residentKey: "required" for discoverable credentials (passwordless-ready). Excludes already-registered credentials.

Request

Headers:
  Authorization: Bearer <accessToken>

Body: (empty)

Response 200

{
  "challenge": "dGVzdC1jaGFsbGVuZ2U...",
  "rp": {
    "name": "eToro",
    "id": "etoro.com"
  },
  "user": {
    "id": "MTIzNDU...",
    "name": "user@email.com",
    "displayName": "Yoni"
  },
  "pubKeyCredParams": [
    { "type": "public-key", "alg": -7 },
    { "type": "public-key", "alg": -257 }
  ],
  "timeout": 120000,
  "attestation": "none",
  "authenticatorSelection": {
    "residentKey": "required",
    "requireResidentKey": true,
    "userVerification": "preferred",
    "authenticatorAttachment": "platform"
  },
  "excludeCredentials": [
    { "id": "existing-cred-base64url", "type": "public-key", "transports": ["internal"] }
  ]
}

Error Responses

StatusCodeDescription
409MAX_CREDENTIALS_REACHEDUser already has 10 passkeys registered
429RATE_LIMIT_EXCEEDEDToo many registration attempts
POST /api/v1/2fa/passkey/register/verify โ€” Verify & Store Passkey

Request

{
  "attestation": {
    "id": "credential-id-base64url",
    "rawId": "Y3JlZGVudGlhbC1pZA...",
    "response": {
      "clientDataJSON": "eyJ0eXBl...",
      "attestationObject": "o2NmbXR...",
      "transports": ["internal", "hybrid"]
    },
    "type": "public-key",
    "clientExtensionResults": {
      "credProps": {
        "rk": true
      }
    },
    "authenticatorAttachment": "platform"
  },
  "deviceName": "iPhone 15 Pro"
}

Response 200

{
  "registered": true,
  "credentialId": "credential-id-base64url",
  "deviceName": "iPhone 15 Pro",
  "deviceType": "multiDevice",
  "backupEligible": true,
  "backupState": true,
  "transports": ["internal", "hybrid"]
}

Server-side: Stores credential_id, public_key, sign_count=0, transports, aaguid, backup_eligible (BE flag from authenticatorData), backup_state (BS flag). If this is user's first passkey, also generates backup codes.

POST /api/v1/2fa/passkey/authenticate/options โ€” Get Auth Options

For standard 2FA: pass tempToken and get allowCredentials with user's registered passkeys. For passwordless/discoverable: omit tempToken and send empty allowCredentials.

Request (2FA mode)

Headers:
  X-Temp-Token: <tempToken>

Body: (empty)

Request (Passwordless mode)

Headers: (none)

Body: (empty)
// Returns empty allowCredentials
// for discoverable credential flow

Response 200 (2FA)

{
  "challenge": "YXV0aC1jaGFsbGVuZ2U...",
  "allowCredentials": [
    {
      "id": "cred-1-base64url",
      "type": "public-key",
      "transports": ["internal", "hybrid"]
    },
    {
      "id": "cred-2-base64url",
      "type": "public-key",
      "transports": ["usb"]
    }
  ],
  "timeout": 120000,
  "userVerification": "preferred",
  "rpId": "etoro.com"
}

Response 200 (Passwordless)

{
  "challenge": "cGFzc3dvcmRsZXNz...",
  "allowCredentials": [],
  "timeout": 120000,
  "userVerification": "required",
  "rpId": "etoro.com"
}
POST /api/v1/2fa/passkey/authenticate/verify โ€” Verify Assertion

Request

{
  "assertion": {
    "id": "credential-id-base64url",
    "rawId": "Y3JlZGVudGlhbC1pZA...",
    "response": {
      "clientDataJSON": "eyJ0eXBl...",
      "authenticatorData": "SZYN5YgO...",
      "signature": "MEUCIQC...",
      "userHandle": "MTIzNDU..."
    },
    "type": "public-key",
    "clientExtensionResults": {},
    "authenticatorAttachment": "platform"
  }
}

Response 200

{
  "accessToken": "eyJhbG...",
  "refreshToken": "eyJhbG...",
  "expiresIn": 3600,
  "credentialUsed": {
    "id": "credential-id-base64url",
    "deviceName": "iPhone 15 Pro"
  }
}

Server-side: Verifies signature, checks sign_count (must be > stored value or alert clone), updates last_used_at and sign_count. For passwordless: resolves user from userHandle in assertion.

GET /api/v1/2fa/passkey/credentials โ€” List Passkeys

Response 200

{
  "credentials": [
    {
      "id": "cred-id-1",
      "deviceName": "iPhone 15 Pro",
      "deviceType": "multiDevice",
      "backupEligible": true,
      "backupState": true,
      "transports": ["internal", "hybrid"],
      "lastUsed": "2026-02-22T10:30:00Z",
      "createdAt": "2026-01-15T09:00:00Z"
    },
    {
      "id": "cred-id-2",
      "deviceName": "YubiKey 5",
      "deviceType": "singleDevice",
      "backupEligible": false,
      "backupState": false,
      "transports": ["usb", "nfc"],
      "lastUsed": "2026-02-20T14:00:00Z",
      "createdAt": "2026-02-01T11:00:00Z"
    }
  ],
  "maxCredentials": 10
}
DELETE /api/v1/2fa/passkey/credentials/:id โ€” Remove Passkey

Request

DELETE /api/v1/2fa/passkey/credentials/cred-id-2

Headers:
  Authorization: Bearer <accessToken>
  X-2FA-Code: 738204  // Current TOTP or backup code

Response 200

{
  "deleted": true,
  "credentialId": "cred-id-2",
  "remainingCredentials": 1
}

Error Responses

StatusCodeDescription
400CANNOT_DELETE_LAST_METHODCannot delete last passkey if it's the only 2FA method
404CREDENTIAL_NOT_FOUNDCredential ID not found for this user
POST /api/v1/2fa/passkey/rename โ€” Rename Passkey

Request

{
  "credentialId": "cred-id-1",
  "deviceName": "Work Phone"
}

Response 200

{
  "credentialId": "cred-id-1",
  "deviceName": "Work Phone",
  "updated": true
}
POST /api/v1/2fa/backup-codes/generate

Request

Headers:
  Authorization: Bearer <accessToken>

{ "currentCode": "738204" }

Response 200

{
  "codes": [
    "A1B2-C3D4", "E5F6-G7H8",
    "I9J0-K1L2", "M3N4-O5P6",
    "Q7R8-S9T0", "U1V2-W3X4",
    "Y5Z6-A7B8", "C9D0-E1F2",
    "G3H4-I5J6", "K7L8-M9N0"
  ],
  "warning": "Store these codes securely. Previous codes are now invalid."
}
POST /api/v1/2fa/backup-codes/verify

Request

{
  "code": "A1B2-C3D4"
}

Response 200

{
  "accessToken": "eyJhbG...",
  "refreshToken": "eyJhbG...",
  "codesRemaining": 9,
  "warning": "You have 9 backup codes remaining"
}
GET /api/v1/2fa/status

Response 200

{
  "enabled": true,
  "primaryMethod": "totp",
  "totp": { "enabled": true, "configuredAt": "2026-02-22T14:00:00Z" },
  "webauthn": {
    "enabled": true,
    "credentials": [
      {
        "id": "cred-id-1",
        "deviceName": "iPhone 15 Pro",
        "lastUsed": "2026-02-22T10:00:00Z",
        "createdAt": "2026-01-15T09:00:00Z",
        "backedUp": true
      }
    ]
  },
  "backupCodes": { "remaining": 8, "generatedAt": "2026-02-22T14:00:00Z" },
  "recovery": { "email": "y***@etoro.com", "phone": "+972***1010" }
}
POST /api/v1/2fa/recovery

Request

{
  "email": "user@email.com",
  "identityProof": "base64-encoded-id-photo",
  "reason": "lost_device"
}

Response 202

{
  "recoveryId": "rec-uuid-123",
  "status": "pending_review",
  "estimatedTime": "24-48 hours",
  "message": "Recovery request submitted. You'll receive an email with next steps."
}

Recovery requires manual review by security team. User must verify identity via email link + ID document.

๐Ÿ“– User Stories

US-001: Enable TOTP Authentication

As a registered eToro user, I want to set up TOTP-based 2FA, so that my account has an additional layer of security.

  • โœ… User can access 2FA settings from account security page
  • โœ… System generates a TOTP secret and displays QR code
  • โœ… User must verify a valid code before 2FA is activated
  • โœ… 10 backup codes are generated and displayed once
  • โœ… Audit log entry created on successful enable

US-002: Login with TOTP

As a user with TOTP enabled, I want to enter my authenticator code after password, so that my login is protected.

  • โœ… After password validation, user is prompted for TOTP code
  • โœ… Code accepts ยฑ1 time step window (90 seconds)
  • โœ… Same code cannot be reused (replay protection)
  • โœ… 5 failed attempts triggers 5-minute lockout
  • โœ… Option to use backup code or passkey instead

US-003: Register Passkey

As a user, I want to register my device's biometric as a passkey, so that I can log in without typing codes.

  • โœ… User can add multiple passkeys (up to 10)
  • โœ… Device biometric prompt (Face ID/fingerprint/Windows Hello) shown
  • โœ… Credential stored with user-friendly device name (auto-suggested, editable)
  • โœ… Resident key / discoverable credential enabled for passwordless-ready
  • โœ… Works alongside TOTP (user chooses at login)
  • โœ… Passkey synced via iCloud Keychain / Google Password Manager automatically
  • โœ… backup_eligible and backup_state flags stored for credential management

US-004: Login with Passkey

As a user with registered passkeys, I want to authenticate using biometrics, so that login is fast and phishing-proof.

  • โœ… After password, "Sign in with Passkey" shown as preferred option
  • โœ… Biometric prompt appears with registered credential
  • โœ… Sign counter is incremented (clone detection)
  • โœ… Falls back to TOTP โ†’ backup code โ†’ recovery if passkey fails
  • โœ… Cross-device auth via QR code (hybrid transport) when on unfamiliar device

US-004b: Passwordless Login with Passkey (Phase 3)

As a user with a discoverable passkey, I want to sign in without entering my username or password, so that login is instant.

  • โœ… Login page supports conditional UI (WebAuthn autofill)
  • โœ… Browser shows passkey suggestion in username field
  • โœ… User taps suggestion โ†’ biometric โ†’ logged in (no password)
  • โœ… userHandle in assertion maps to eToro user ID
  • โœ… Audit log records passwordless_login event

US-004c: Manage Passkeys

As a user with registered passkeys, I want to view, rename, and delete my passkeys, so that I can manage my devices.

  • โœ… List shows device name, last used date, created date, sync status
  • โœ… User can rename passkeys (e.g., "iPhone 15 Pro" โ†’ "Work Phone")
  • โœ… User can delete passkeys (requires 2FA verification)
  • โœ… Cannot delete last passkey if it's the only 2FA method
  • โœ… Warning when deleting a synced passkey (affects all synced devices)

US-004d: Cross-Device Passkey Authentication

As a user logging in on a device without my passkey, I want to use my phone as an authenticator via QR code, so that I can securely log in anywhere.

  • โœ… QR code displayed on desktop browser
  • โœ… User scans with phone camera โ†’ BLE proximity check
  • โœ… Biometric prompt on phone โ†’ assertion tunneled to desktop
  • โœ… Works across ecosystems (iPhone authenticating on Windows)

US-004e: Passkey Nudge (Migration)

As a user currently using SMS or TOTP, I want to be prompted to upgrade to Passkey, so that I benefit from better security and UX.

  • โœ… Post-login bottom sheet shown after successful SMS/TOTP login
  • โœ… Shows speed comparison: "Sign in 5x faster with Passkey"
  • โœ… "Set up now" (primary) and "Maybe later" (dismiss) buttons
  • โœ… Dismissed nudge reappears after 7 days (max 3 times then stops)
  • โœ… Nudge suppressed if device doesn't support WebAuthn

US-005: Use Backup Code

As a user who lost access to their authenticator, I want to use a backup code, so that I can still log in.

  • โœ… Each backup code is single-use
  • โœ… Warning shown when โ‰ค3 codes remaining
  • โœ… User prompted to regenerate when โ‰ค2 remaining

US-006: Disable 2FA

As a user, I want to disable 2FA, so that I can simplify my login if needed.

  • โœ… Requires current TOTP code or passkey verification
  • โœ… Confirmation dialog warns about reduced security
  • โœ… All TOTP secrets and backup codes are deleted
  • โœ… WebAuthn credentials optionally retained
  • โœ… Audit log entry created

US-007: Account Recovery

As a user who lost all 2FA methods, I want to recover my account, so that I'm not permanently locked out.

  • โœ… Recovery requires email verification + ID document
  • โœ… Manual review by security team (24-48 hour SLA)
  • โœ… 2FA is temporarily disabled upon approval
  • โœ… User forced to set up new 2FA within 72 hours

โš ๏ธ Error Handling

HTTPError CodeMessageAction
400INVALID_INPUTValidation failedReturn field errors
401INVALID_TOTP_CODEInvalid verification codeIncrement failure counter
401CODE_ALREADY_USEDCode was already usedLog replay attempt
401INVALID_BACKUP_CODEBackup code invalidIncrement failure counter
401WEBAUTHN_VERIFICATION_FAILEDPasskey verification failedLog failure
4032FA_REQUIREDTwo-factor auth requiredRedirect to 2FA prompt
409TOTP_ALREADY_ENABLEDTOTP is already activeโ€”
409MAX_CREDENTIALS_REACHEDMaximum 5 passkeysโ€”
423ACCOUNT_LOCKEDAccount locked due to failed attemptsEmail alert sent
429RATE_LIMIT_EXCEEDEDToo many attemptsReturn retryAfter header
500ENCRYPTION_ERRORInternal security errorAlert engineering + PagerDuty

๐Ÿ“ฆ Dependencies

Runtime

PackageVersionPurpose
otplib^12.0TOTP generation & verification
@simplewebauthn/server^10.0WebAuthn server operations
@nestjs/throttler^5.0Rate limiting
qrcode^1.5QR code generation (optional, client-side)
node:cryptobuilt-inRandom bytes, timing-safe compare
bcrypt^5.1Backup code hashing

Infrastructure

ServicePurpose
PostgreSQL 15+Persistent storage
Redis 7+Rate limiting, challenge cache
HashiCorp VaultTransit encryption of TOTP secrets
DatadogMonitoring & alerting
โ† Research ยท Architecture ยท Client SpecKit โ†’ ยท Swagger โ†’