Skip to content

feat(auth): TOTP MFA with recovery codes and pre-auth step-up - #62

Merged
PenguinzTech merged 1 commit into
v2.1.xfrom
feature/mfa-totp
Jul 27, 2026
Merged

feat(auth): TOTP MFA with recovery codes and pre-auth step-up#62
PenguinzTech merged 1 commit into
v2.1.xfrom
feature/mfa-totp

Conversation

@PenguinzTech

@PenguinzTech PenguinzTech commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

TOTP multi-factor auth for manager users: enroll → activate (returns 8 bcrypt-hashed single-use recovery codes once) → step-up login. Users with MFA get a 5-minute pre-auth JWT scoped only mfa:verify (rejected by all normal endpoints — tested), then /api/v1/auth/mfa-verify issues the real pair. Secrets Fernet-encrypted at rest (HKDF from SECRET_KEY); TOTP replay blocked via last-used timestep; disable requires password + code. pyotp==2.9.0 exact-pinned.

Tests: 171 green (29 new). Login-route diff kept to 9 lines; non-MFA users unaffected.
Merge note: contains alembic migration 008 — collides with 008 in the audit and NHI branches; renumber whichever merges later.

Stack note: bases on chore/dedup-reusable-code (top of the #53#59 chain); auto-retargets toward v2.1.x as the stack merges bottom-up.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce TOTP-based multi-factor authentication with a pre-auth step-up login flow, recovery codes, and supporting database and service changes.

New Features:

  • Add MFA enrollment, activation, verification, and disabling API endpoints for manager users.
  • Gate logins for MFA-enabled users behind a short-lived pre-auth token that must be exchanged via an MFA verification endpoint.
  • Provide single-use recovery codes as an alternative to TOTP codes for MFA login and disable flows.

Enhancements:

  • Introduce an MFA service module for TOTP secret generation, encryption, verification, recovery code management, and pre-auth token handling.
  • Extend the auth_user schema to track MFA state, encrypted TOTP secrets, recovery codes, and last-used TOTP counter for replay protection.

Build:

  • Add pyotp as a pinned dependency for TOTP generation and verification.

Deployment:

  • Add an Alembic migration to add MFA-related columns to the auth_user table.

Tests:

  • Add comprehensive unit and integration tests covering MFA enrollment, activation, verification, replay protection, recovery codes, disable flows, and pre-auth token scope enforcement.

Implements comprehensive TOTP-based multi-factor authentication with:
- TOTP secret generation and provisioning URI for QR codes (pyotp)
- Per-user recovery codes (8 single-use codes, bcrypt hashed)
- Pre-auth token step-up (5-minute JWT, scope='mfa:verify')
- TOTP replay attack prevention (timestamp counter tracking)
- Fernet encryption for secret storage using app SECRET_KEY + HKDF-SHA256
- Full enroll → activate → login → verify → disable lifecycle
- Security: constant-time recovery code comparison, never logs codes/secrets

Files:
- mfa_service.py: core MFA logic (TOTP, recovery codes, tokens, encryption)
- mfa.py blueprint: enroll, activate, disable endpoints + auth/mfa-verify
- auth.py: modified login to return pre_auth_token when mfa_enabled
- schema.py: added mfa_enabled, mfa_secret, mfa_recovery_codes, mfa_last_totp_counter
- 008_add_mfa_fields.py: Alembic migration
- 29 tests: enroll, activate, verify, disable, replay detection, security

All 171 tests pass (169 existing + 2 new security tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech PenguinzTech self-assigned this Jul 25, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown

🧙 Sourcery is reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​pyotp@​2.9.0100100100100100

View full report

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @PenguinzTech, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@PenguinzTech
PenguinzTech changed the base branch from chore/dedup-reusable-code to v2.1.x July 27, 2026 00:54
@PenguinzTech
PenguinzTech merged commit 7f6623e into v2.1.x Jul 27, 2026
3 of 4 checks passed
PenguinzTech added a commit that referenced this pull request Jul 27, 2026
008_add_mfa_fields (#62) and 008_machine_clients/008_audit_events
(already-merged #61/#63) both independently claimed
down_revision=007_revoked_token, creating two divergent heads
(009_oidc_trust_anchors and 008_add_mfa_fields). Git's merge saw no
textual conflict since they're different files, so this surfaced only
after the merge completed.

Re-chained 008_add_mfa_fields after 009_oidc_trust_anchors so the
migration graph has a single linear head again. Full manager suite:
229/229 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech
PenguinzTech deleted the feature/mfa-totp branch July 27, 2026 01:12
PenguinzTech added a commit that referenced this pull request Jul 27, 2026
- .pre-commit-config.yaml: combined the 3-way pre-commit divergence.
  Took this branch's flake8 approach (single .flake8 source of truth
  via --config, precise files: scoping to app/bins dirs, flake8-bugbear
  added) over the earlier same-session emergency inline-args patch on
  v2.1.x. Took this branch's newer valid hadolint tag (v2.14.0) over
  v2.13.1. gitleaks entry was already identical on both sides
  (protect --staged --source .) — no divergence there after all.
- .gitleaks.toml: added an allowlist entry for the well-known
  pyotp/Google-Authenticator tutorial TOTP secret (base32 for
  "Hello!!!"), which #62 (MFA, merged after this branch was created)
  uses in docstrings/tests — not a real credential.
- dhcp-server test-isolation fix (found during this merge's full-suite
  re-verification, not part of either branch's original diff):
  test_dhcp_allocation.py imports app.db at collection time, which
  imports app.config and caches JWT_PUBLIC_KEY=None before any fixture
  sets the env var; later test_auth.py tests then ran against the
  stale cached None. Fixed by clearing app.*/bins.server from
  sys.modules in the setup_env fixture so every test gets a fresh
  import after env vars are set. Test-harness-only — the real server
  imports config exactly once at actual startup.
- flake8-bugbear (newly enforced by this merge) caught 3 genuine minor
  issues in the kid-rotation code: an unused import and unused loop
  variables (kid_val -> _kid_val) in dhcp-server/app/auth.py and
  ntp-server/bins/server.py's multi-key verification loops.

Full suite after combine: manager 229/229, dns-server 63/63,
dhcp-server 32/32 (order-independent), ntp-server 51/51,
squawk-client 13/13. flake8 clean, gitleaks clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.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