Skip to content

Commit 8113ba3

Browse files
os-samclaude
andauthored
test(dogfood): converge two-factor-lockout on the shared TOTP helper (#11323)
`two-factor-lockout.dogfood.test.ts` carried a private `base32Decode` / `totp` pair, byte-for-byte identical to the ones in `test/totp.ts` — the shared module was extracted FROM this file (#10681) without rewriting its internals, so the package held two spellings of RFC 6238. Point the file at `./totp.js`, delete both local functions, and collapse the inline otpauth:// parsing into `secretFromTotpUri`. Mechanical: no assertion, timeout or fixture-semantics changes. The hand-rolled rationale (`@better-auth/utils/otp` is a transitive dependency; taking a direct one to make six digits would tie these fixtures to an internal package's resolution) already lives in the shared module's header, so it survives. That header's stale "the lockout file still carries its own copy" warning is replaced by the fact that this is now the package's single copy. Verified the file really drives the shared helper rather than merely importing it: mutating the shared `totp()` to return a fixed wrong code turns the suite red in `beforeAll` (enrolment confirmation, 401 INVALID_CODE vs the expected 200); restoring it byte-identically returns it to 5 passed. Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7233907 commit 8113ba3

2 files changed

Lines changed: 7 additions & 48 deletions

File tree

packages/qa/dogfood/test/totp.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
* defaults are the RFC's (SHA-1, 6 digits, 30s), and `enable`'s own otpauth://
1111
* URI asserts them.
1212
*
13-
* ⚠️ `two-factor-lockout.dogfood.test.ts` still carries its own private copy of
14-
* these two functions — this module was extracted while adding a second caller
15-
* (#10681) and deliberately did NOT rewrite that file's internals, since it pins
16-
* an unrelated card. Consolidating it is filed separately.
13+
* This is the package's SINGLE copy. It was extracted from
14+
* `two-factor-lockout.dogfood.test.ts` while adding a second caller (#10681),
15+
* which kept a private copy of its own until #11111 converged it here. Need a
16+
* TOTP in a new fixture? Import it from this module — do not paste a third
17+
* spelling of RFC 6238 into the package.
1718
*/
1819

1920
import { createHmac } from 'node:crypto';

packages/qa/dogfood/test/two-factor-lockout.dogfood.test.ts

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
*/
4545

4646
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
47-
import { createHmac } from 'node:crypto';
4847
import showcaseStack from '@objectstack/example-showcase';
4948
import { bootStack, type VerifyStack } from '@objectstack/verify';
5049
import { assertArmed, authSettingArmed } from './armed.js';
50+
import { secretFromTotpUri, totp } from './totp.js';
5151

5252
const SYS = { context: { isSystem: true } };
5353
const ADMIN_PASSWORD = 'admin123';
@@ -65,46 +65,6 @@ const LOCKOUT_DURATION_MINUTES = 40;
6565
/** better-auth's per-two-factor-cookie cap. Hardcoded upstream, not configurable. */
6666
const MAX_PER_CHALLENGE = 5;
6767

68-
// ── RFC 6238 TOTP ──────────────────────────────────────────────────────────
69-
// Hand-rolled rather than imported: `@better-auth/utils/otp` is a transitive
70-
// dependency, and adding it as a direct one to generate six digits would tie
71-
// this test to an internal package's resolution. better-auth's defaults are
72-
// the RFC's (SHA-1, 6 digits, 30s), asserted by `enable`'s own otpauth:// URI.
73-
74-
function base32Decode(input: string): Buffer {
75-
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
76-
const clean = input.replace(/=+$/, '').toUpperCase();
77-
let bits = 0;
78-
let value = 0;
79-
const out: number[] = [];
80-
for (const char of clean) {
81-
const idx = ALPHABET.indexOf(char);
82-
if (idx === -1) throw new Error(`invalid base32 character: ${char}`);
83-
value = (value << 5) | idx;
84-
bits += 5;
85-
if (bits >= 8) {
86-
out.push((value >>> (bits - 8)) & 0xff);
87-
bits -= 8;
88-
}
89-
}
90-
return Buffer.from(out);
91-
}
92-
93-
/** The 6-digit TOTP for `secret` at the current 30-second step. */
94-
function totp(secret: Buffer): string {
95-
const counter = Math.floor(Date.now() / 30_000);
96-
const buf = Buffer.alloc(8);
97-
buf.writeBigUInt64BE(BigInt(counter));
98-
const digest = createHmac('sha1', secret).update(buf).digest();
99-
const offset = digest[digest.length - 1] & 0x0f;
100-
const code =
101-
((digest[offset] & 0x7f) << 24) |
102-
((digest[offset + 1] & 0xff) << 16) |
103-
((digest[offset + 2] & 0xff) << 8) |
104-
(digest[offset + 3] & 0xff);
105-
return String(code % 1_000_000).padStart(6, '0');
106-
}
107-
10868
/** Collect a response's Set-Cookie values into a single request Cookie header. */
10969
function cookieHeader(res: Response): string {
11070
const jar = res.headers.getSetCookie?.() ?? [];
@@ -180,9 +140,7 @@ describe('#3624 follow-up: better-auth 2FA lockout counts wrong codes', () => {
180140
});
181141
expect(enabled.status, `two-factor/enable: ${await enabled.clone().text()}`).toBe(200);
182142
const { totpURI } = (await enabled.json()) as { totpURI: string };
183-
const uriSecret = new URL(totpURI.replace('otpauth://', 'https://')).searchParams.get('secret');
184-
expect(uriSecret, 'no secret in the otpauth URI').toBeTruthy();
185-
secret = base32Decode(uriSecret as string);
143+
secret = secretFromTotpUri(totpURI);
186144

187145
// better-auth enrols with `verified: false`, and the sign-in path refuses
188146
// an unverified enrolment (TOTP_NOT_ENABLED) before it ever reaches the

0 commit comments

Comments
 (0)