Skip to content

Commit e705b0c

Browse files
authored
fix(devframe): defer auth code generation (#345)
1 parent e791666 commit e705b0c

2 files changed

Lines changed: 38 additions & 5 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest'
2+
3+
describe('auth state import', () => {
4+
afterEach(() => {
5+
vi.doUnmock('devframe/utils/crypto-token')
6+
vi.resetModules()
7+
})
8+
9+
it('does not generate an authentication code at module evaluation', async () => {
10+
const randomDigits = vi.fn(() => '123456')
11+
12+
vi.doMock('devframe/utils/crypto-token', () => ({
13+
randomDigits,
14+
randomToken: vi.fn(),
15+
timingSafeEqual: vi.fn(),
16+
}))
17+
18+
await import('../state')
19+
20+
expect(randomDigits).not.toHaveBeenCalled()
21+
})
22+
})

packages/devframe/src/node/auth/state.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,29 @@ const TEMP_AUTH_CODE_TTL = 5 * 60_000
3838
/** Failed attempts allowed against a single code before it is rotated. */
3939
const TEMP_AUTH_MAX_ATTEMPTS = 5
4040

41-
let tempAuthCode: string = generateTempCode()
42-
let tempAuthCodeExpiresAt: number = Date.now() + TEMP_AUTH_CODE_TTL
41+
let tempAuthCode: string | undefined
42+
let tempAuthCodeExpiresAt = 0
4343
let tempAuthFailedAttempts = 0
4444

4545
function generateTempCode(): string {
4646
return randomDigits(TEMP_AUTH_CODE_LENGTH)
4747
}
4848

49+
function ensureTempAuthCode(): string {
50+
if (tempAuthCode === undefined) {
51+
tempAuthCode = generateTempCode()
52+
tempAuthCodeExpiresAt = Date.now() + TEMP_AUTH_CODE_TTL
53+
}
54+
55+
return tempAuthCode
56+
}
57+
4958
/**
5059
* The current one-time authentication code. Display this to the user (e.g. in
5160
* the dev-server terminal) so they can type it into the browser to authenticate.
5261
*/
5362
export function getTempAuthCode(): string {
54-
return tempAuthCode
63+
return ensureTempAuthCode()
5564
}
5665

5766
/**
@@ -78,7 +87,7 @@ export function refreshTempAuthCode(): string {
7887
* `Referer` header; the browser client reads it locally (see
7988
* `consumeOtpFromUrl`). Any existing fragment parameters are preserved.
8089
*/
81-
export function buildOtpAuthUrl(baseUrl: string, code: string = tempAuthCode): string {
90+
export function buildOtpAuthUrl(baseUrl: string, code: string = getTempAuthCode()): string {
8291
const url = new URL(baseUrl)
8392
const fragment = new URLSearchParams(url.hash.replace(/^#/, ''))
8493
fragment.set(DEVFRAME_OTP_URL_PARAM, code)
@@ -125,13 +134,15 @@ export function exchangeTempAuthCode(
125134
info: { ua: string, origin: string },
126135
storage: SharedState<InternalAnonymousAuthStorage>,
127136
): string | null {
137+
const currentTempAuthCode = ensureTempAuthCode()
138+
128139
// Expired code: rotate so a stale code can never be redeemed.
129140
if (Date.now() > tempAuthCodeExpiresAt) {
130141
refreshTempAuthCode()
131142
return null
132143
}
133144

134-
if (!timingSafeEqual(code, tempAuthCode)) {
145+
if (!timingSafeEqual(code, currentTempAuthCode)) {
135146
tempAuthFailedAttempts += 1
136147
// Too many wrong guesses, so invalidate this code entirely.
137148
if (tempAuthFailedAttempts >= TEMP_AUTH_MAX_ATTEMPTS)

0 commit comments

Comments
 (0)