Skip to content

feat(verification): hashed one-time codes with attempt counting - #289

Merged
roncodes merged 1 commit into
release/v1.6.69from
feature/hashed-verification-codes
Oct 7, 2026
Merged

roncodes merged 1 commit into
release/v1.6.69from
feature/hashed-verification-codes

Conversation

@roncodes

@roncodes roncodes commented Oct 6, 2026

Copy link
Copy Markdown
Member

PR 1 of the customer tracking pages refactor. It adds hashed one-time codes to VerificationCode, for flows where a leaked verification_codes table must not give away live codes.

Why

VerificationCode stores codes in plain text and counts no attempts. The new public tracking page sends recipients a 6-digit code to unlock delivery details, so it needs codes that are stored hashed and can't be guessed indefinitely.

Changes

  • VerificationCode::issue($subject, $for, $options):
    • Stores hash_hmac('sha256', code, app.key), the same keying TwoFactorAuth uses.
    • Returns the plain code once, on the instance's plainCode property. It is never persisted.
    • Defaults to a 10-minute expiry and an active status. meta is merged, and hashed and attempts are recorded.
  • check($plainCode, $maxAttempts = 3): returns valid, invalid, expired or locked. It compares with hash_equals, counts wrong attempts in meta, and locks the code on the last allowed attempt. It also checks plain codes made the old way.
  • attemptsLeft() and hashCode().
  • The creating hook now keeps a code that was already set, so issue() isn't overwritten. Codes made the old way still get a random one.

Existing generators (generateFor, generateEmailVerificationFor, generateSmsVerificationFor) and their callers don't change.

Tests

Three cases added to VerificationCodeModelTest:

  • the stored value is the HMAC, not the plain code, and defaults and overrides apply;
  • the attempt lifecycle: wrong, right, wrong, locked, then still locked with the right code;
  • expiry at the boundary, and old-style plain codes.

Add VerificationCode::issue(), check() and attemptsLeft() for flows where a
leaked table must not give away live codes:

- issue() stores an HMAC of the code, keyed by the app key, and hands the
  plain code back once on the instance (plainCode), defaulting to a 10-minute
  expiry and an 'active' status.
- check() compares with hash_equals, counts wrong attempts in meta and locks
  the code on the last allowed one. Expired and locked codes report as such.
- The creating hook keeps a code that was already set, so issue() is not
  overwritten; codes made the old way still get a random one and still check.

Existing generators and their callers are unchanged. First user: the FleetOps
public tracking page's one-time codes.
@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ab33100) to head (81b0245).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##                main      #289   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity      7931      7943   +12     
===========================================
  Files            438       438           
  Lines          25665     25698   +33     
===========================================
+ Hits           25665     25698   +33     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roncodes roncodes mentioned this pull request Oct 7, 2026
@roncodes
roncodes changed the base branch from main to release/v1.6.69 October 7, 2026 05:51
@roncodes
roncodes merged commit b8e6c2f into release/v1.6.69 Oct 7, 2026
7 checks passed
@roncodes
roncodes deleted the feature/hashed-verification-codes branch October 7, 2026 05:54
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