Skip to content

feat: two-factor authentication (TOTP + recovery codes) - #121

Merged
mpge merged 1 commit into
mainfrom
feat/two-factor-auth
Aug 1, 2026
Merged

feat: two-factor authentication (TOTP + recovery codes)#121
mpge merged 1 commit into
mainfrom
feat/two-factor-auth

Conversation

@mpge

@mpge mpge commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Adds two-factor authentication (TOTP + recovery codes) to the AdonisJS backend, which previously had no 2FA support (it was referenced only in a code comment). Ports the Laravel/Symfony reference contract.

What's included

  • TwoFactorService (src/services/two_factor_service.ts) — RFC-6238 TOTP (HMAC-SHA1, 6 digits, 30s period) implemented with Node's built-in crypto module, no new OTP dependency. Provides:
    • base32 secret generation
    • otpauth:// enrollment URI (for QR display)
    • verify() with a +/- 1 time-slice clock-drift window (constant-time compare)
    • single-use recovery codes (hashed with SHA-256)
    • AES-256-GCM encryption of the secret at rest
  • TwoFactor Lucid model (src/models/two_factor.ts) + escalated_two_factor migration — encrypted secret, hashed recovery_codes, confirmed_at; unique per user, mirroring how api_token is modeled. Uses the userIdColumn helper so it honors the host user key type (int/uuid/string).
  • AdminTwoFactorControllerindex, setup, confirm, disable, regenerateRecoveryCodes, plus a post-login challenge/verify path (accepts a TOTP code or a single-use recovery code).
  • Routes — management routes under admin/settings/two-factor/*, and an auth-only two-factor/challenge group for the login challenge.

Contract

Generate a TOTP secret → expose an otpauth:// URI for enrollment → confirm by verifying a first code → store secret (encrypted) + hashed recovery codes + confirmed_at → verify a TOTP or recovery code at challenge time → disable + regenerate recovery codes.

Tests

New unit spec tests/unit/two_factor_service.spec.ts (16 tests), including:

  • secret generation shape + uniqueness, otpauth URI
  • RFC 6238 known-answer TOTP vectors (t=59, 1111111109, 1234567890, 2000000000)
  • correct-code accepted (current + prior slice), far-slice + malformed rejected
  • recovery-code single-use (a code works once, then is removed)
  • secret encrypt/decrypt round-trip

Full suite green with no regressions: 565 node:test tests + 21 Japa tests pass. tsc --noEmit, ESLint, and Prettier (3.9.6) all clean on the changed files.

Port the Laravel/Symfony 2FA reference to the AdonisJS backend, which
previously had no 2FA support.

- TwoFactorService: RFC-6238 TOTP (HMAC-SHA1, 6 digits, 30s period)
  implemented with node:crypto (no new OTP dependency), plus base32 secret
  generation, otpauth:// enrollment URIs, single-use recovery codes, and
  AES-256-GCM secret encryption at rest.
- TwoFactor Lucid model + escalated_two_factor migration (encrypted secret,
  hashed recovery codes, confirmed_at), mirroring the api_token store.
- AdminTwoFactorController: setup, confirm, disable, regenerate recovery
  codes, and a post-login challenge/verify path (TOTP or recovery code).
- Routes under admin settings plus an auth-only two-factor challenge group.
- Unit spec covering RFC-6238 known-answer vectors, wrong-code rejection,
  and recovery-code single-use.
@mpge
mpge merged commit 7463d4d into main Aug 1, 2026
4 checks passed
@mpge
mpge deleted the feat/two-factor-auth branch August 1, 2026 14:28
mpge added a commit that referenced this pull request Aug 1, 2026
Port the Laravel/Symfony 2FA reference to the AdonisJS backend, which
previously had no 2FA support.

- TwoFactorService: RFC-6238 TOTP (HMAC-SHA1, 6 digits, 30s period)
  implemented with node:crypto (no new OTP dependency), plus base32 secret
  generation, otpauth:// enrollment URIs, single-use recovery codes, and
  AES-256-GCM secret encryption at rest.
- TwoFactor Lucid model + escalated_two_factor migration (encrypted secret,
  hashed recovery codes, confirmed_at), mirroring the api_token store.
- AdminTwoFactorController: setup, confirm, disable, regenerate recovery
  codes, and a post-login challenge/verify path (TOTP or recovery code).
- Routes under admin settings plus an auth-only two-factor challenge group.
- Unit spec covering RFC-6238 known-answer vectors, wrong-code rejection,
  and recovery-code single-use.

Co-authored-by: Matt Gros <mpge@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant