Skip to content

Count only failed MFA attempts toward the rate limit - #192

Merged
ndbroadbent merged 2 commits into
mainfrom
nathan/mfa-limit-failures
Oct 26, 2026
Merged

ndbroadbent merged 2 commits into
mainfrom
nathan/mfa-limit-failures

Conversation

@ndbroadbent

@ndbroadbent ndbroadbent commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

The MFA rate limit (five attempts per kind of factor in five minutes) counted every attempt: successful ones, and the refused ones too. Every run, exec and deploy approval needs its own MFA proof, so a person running a few commands, or approving deploys for a few apps, went past five in five minutes. After a good PIN and touch they got "Security key verification failed" (401 mfa_verification_failed), or "websocket: bad handshake" for an exec. Each refused retry added another attempt, so retrying kept them locked out.

Fix

  • ReserveMFAAttempt counts only attempts that failed or are still being checked. Successful attempts don't count, and neither do refused (rate_limited) ones. Concurrent attempts are still counted one at a time under the user's row lock, so the limit still caps guesses at a code.
  • A refused attempt is still recorded, and it's answered with 429 "Too many failed MFA attempts. Wait 5 minutes and try again." on the gateway's routes (mfa_rate_limited) and on the rack proxy, instead of a verification failure.
  • The automatic lock after repeated failures is unchanged.

Testing

  • New TestReserveMFAAttemptCountsOnlyFailedAndUnfinishedAttempts: ten successes, then the limit is reached by unfinished and failed attempts only, and refused attempts don't count. It fails against the old query.
  • New TestApproveDeployApprovalRequest_RateLimitedMFA: a rate-limited approval answers 429 mfa_rate_limited and the request stays pending.
  • The db, MFA, middleware, proxy and handler packages pass.

Includes the bump to v0.1.25.

Summary by CodeRabbit

  • Bug Fixes

    • MFA rate limits now count failed and unfinished verification attempts, but not successful verifications. Limits apply separately to each factor.
    • After five counted attempts within five minutes, further attempts receive a rate-limit response and must wait before retrying.
    • Rate-limited MFA checks for deploy approvals now return a clear error while leaving the approval pending.
  • Documentation

    • Clarified that separate MFA proof is required for each run, exec, and deploy approval.

The limit of five attempts per kind of factor in five minutes counted every attempt,
successful and refused ones included. Every run, exec and deploy approval needs its own MFA
proof, so a person running a few commands, or approving deploys for a few apps, went past
five in five minutes, and each refused retry kept the window full. Their commands failed
after a good PIN and touch, with "Security key verification failed" (or "websocket: bad
handshake" for an exec).

Only attempts that failed or are still being checked count now. A refused attempt is still
recorded, and is answered with 429 "Too many failed MFA attempts" instead of a verification
failure.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: dc54832e-ad44-4baa-970d-49665e4ae5f7
Commits

Reviewing files that changed from the base of the PR and between a32e31b and 501f8b4.

Files selected for processing: 10
  • convox.yml
  • docs/src/content/docs/concepts/mfa-security.mdx
  • internal/gateway/auth/mfa/attempts.go
  • internal/gateway/auth/mfa/security_test.go
  • internal/gateway/db/mfa_attempts.go
  • internal/gateway/db/mfa_attempts_test.go
  • internal/gateway/handlers/deploy_approval_requests_mfa_test.go
  • internal/gateway/middleware/mfa.go
  • internal/gateway/proxy/mfa_verification.go
  • web/package.json

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Walkthrough

MFA attempt limits now count failed and unfinished attempts, but exclude successful and rate-limited attempts. Rate-limit errors return HTTP 429 in gateway paths. Documentation and tests describe or verify the behavior. Gateway, admin, and web version identifiers change to 0.1.25.

Changes

MFA attempt rate limits

Layer / File(s) Summary
Attempt accounting and error propagation
internal/gateway/db/mfa_attempts.go, internal/gateway/db/mfa_attempts_test.go, internal/gateway/auth/mfa/attempts.go, internal/gateway/auth/mfa/security_test.go, docs/src/content/docs/concepts/mfa-security.mdx
The reservation count includes failed and unfinished attempts, but excludes successful and rate-limited attempts. Tests cover attempt status and factor type. Auth-layer errors wrap the limit error, and the documentation describes the limit.
Rate-limit responses
internal/gateway/middleware/mfa.go, internal/gateway/proxy/mfa_verification.go, internal/gateway/handlers/deploy_approval_requests_mfa_test.go
MFA middleware and request verification return HTTP 429 for attempt-limit errors. A deploy approval test checks the response and confirms the approval remains pending.

Version updates

Layer / File(s) Summary
Release identifiers
convox.yml, web/package.json
The gateway and admin image tags and the web package version change to 0.1.25.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant verifyByType
  participant ReserveMFAAttempt
  participant handleVerifyError
  verifyByType->>ReserveMFAAttempt: reserve MFA attempt
  ReserveMFAAttempt-->>verifyByType: return attempt-limit error
  verifyByType->>handleVerifyError: pass verification error
  handleVerifyError-->>verifyByType: return HTTP 429 and mfa_rate_limited
Loading

Possibly related PRs

Suggested reviewers: claude

Merge Risk: Minimal · up to 501f8

No actionable merge-blocking risk remains after the reviewed MFA rate-limit changes and normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 7 files. (3 skipped: 3… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "Count only failed MFA attempts toward the rate limit" matches the main change. The PR changes the MFA rate limit so that only failed or unfinished attempts count. Successful attempts and re…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "Count only failed MFA attempts toward the rate limit" matches the main change. The PR changes the MFA rate limit so that only failed or unfinished attempts count. Successful attempts and refused (rate_limited) attempts no longer count. The changes in internal/gateway/db/mfa_attempts.go, the new tests, and the middleware and proxy error handling all support this. The title is specific and short. It does not mention the version bump or the new 429 handling, which are secondary.

Full details: Docstring Coverage

Explanation

Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 7 files. (3 skipped: 3 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit checks the counters twice
Failed and waiting proofs take space
Success slips past the counting gate
Too many knocks bring 429
The gateway sets its tags anew
And carrots wait for code review

Comment @coderabbitai help to get the list of available commands.

@deepsource-io

deepsource-io Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in a32e31b...501f8b4 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Oct 26, 2026 1:33a.m. Review ↗
Go Oct 26, 2026 1:33a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@ndbroadbent
ndbroadbent merged commit c7445a1 into main Oct 26, 2026
14 checks passed
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