From ee8f9f2bf041857a092f5c016ab8b0acb441ccfa Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 6 May 2026 11:27:59 -0700 Subject: [PATCH] Fix Freebuff CLI auth code handling --- .../web/src/app/api/auth/cli/code/route.ts | 10 ++++-- freebuff/web/src/app/onboard/_helpers.ts | 19 ++++++++++- freebuff/web/src/app/onboard/page.tsx | 14 ++++++++ web/src/app/api/auth/cli/code/route.ts | 11 +++++-- web/src/app/onboard/__tests__/helpers.test.ts | 33 +++++++++++-------- web/src/app/onboard/_helpers.ts | 19 ++++++++++- 6 files changed, 86 insertions(+), 20 deletions(-) diff --git a/freebuff/web/src/app/api/auth/cli/code/route.ts b/freebuff/web/src/app/api/auth/cli/code/route.ts index ac7ac073c..8e254d76d 100644 --- a/freebuff/web/src/app/api/auth/cli/code/route.ts +++ b/freebuff/web/src/app/api/auth/cli/code/route.ts @@ -53,12 +53,18 @@ export async function POST(req: Request) { ) } - const loginUrl = `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}` + // Generate login URL on the same origin that issued the auth code. This + // avoids bouncing between apex/www hosts during the browser OAuth flow. + const loginUrl = new URL('/login', new URL(req.url).origin) + loginUrl.searchParams.set( + 'auth_code', + `${fingerprintId}.${expiresAt}.${fingerprintHash}`, + ) return NextResponse.json({ fingerprintId, fingerprintHash, - loginUrl, + loginUrl: loginUrl.toString(), expiresAt, }) } catch (error) { diff --git a/freebuff/web/src/app/onboard/_helpers.ts b/freebuff/web/src/app/onboard/_helpers.ts index e26a93d67..d502d0d20 100644 --- a/freebuff/web/src/app/onboard/_helpers.ts +++ b/freebuff/web/src/app/onboard/_helpers.ts @@ -5,7 +5,24 @@ export function parseAuthCode(authCode: string): { expiresAt: string receivedHash: string } { - const [fingerprintId, expiresAt, receivedHash] = authCode.split('.') + const normalizedAuthCode = authCode.trim() + const hashSeparatorIndex = normalizedAuthCode.lastIndexOf('.') + const expiresSeparatorIndex = normalizedAuthCode.lastIndexOf( + '.', + hashSeparatorIndex - 1, + ) + + if (hashSeparatorIndex === -1 || expiresSeparatorIndex === -1) { + return { fingerprintId: '', expiresAt: '', receivedHash: '' } + } + + const fingerprintId = normalizedAuthCode.slice(0, expiresSeparatorIndex) + const expiresAt = normalizedAuthCode.slice( + expiresSeparatorIndex + 1, + hashSeparatorIndex, + ) + const receivedHash = normalizedAuthCode.slice(hashSeparatorIndex + 1) + return { fingerprintId, expiresAt, receivedHash } } diff --git a/freebuff/web/src/app/onboard/page.tsx b/freebuff/web/src/app/onboard/page.tsx index 69dba7284..287b761f4 100644 --- a/freebuff/web/src/app/onboard/page.tsx +++ b/freebuff/web/src/app/onboard/page.tsx @@ -100,6 +100,20 @@ const Onboard = async ({ searchParams }: PageProps) => { ) if (!valid) { + logger.warn( + { + authCodeLength: authCode.length, + fingerprintIdPrefix: fingerprintId.slice(0, 24), + fingerprintIdLength: fingerprintId.length, + expiresAt, + receivedHashPrefix: receivedHash.slice(0, 12), + receivedHashLength: receivedHash.length, + expectedHashPrefix: fingerprintHash.slice(0, 12), + expectedHashLength: fingerprintHash.length, + }, + 'Invalid Freebuff CLI auth code', + ) + return ( { }) test('handles auth code with dots in fingerprint id', () => { - // Note: This is a potential edge case - the current implementation - // only splits into 3 parts, so extra dots would be included in fingerprintId const authCode = 'fp.with.dots.1704067200000.hashvalue' const result = parseAuthCode(authCode) - expect(result.fingerprintId).toBe('fp') - expect(result.expiresAt).toBe('with') - expect(result.receivedHash).toBe('dots') + expect(result.fingerprintId).toBe('fp.with.dots') + expect(result.expiresAt).toBe('1704067200000') + expect(result.receivedHash).toBe('hashvalue') + }) + + test('trims surrounding whitespace from copied auth code', () => { + const authCode = '\n fingerprint-123.1704067200000.abc123hash \t' + const result = parseAuthCode(authCode) + + expect(result.fingerprintId).toBe('fingerprint-123') + expect(result.expiresAt).toBe('1704067200000') + expect(result.receivedHash).toBe('abc123hash') }) test('handles empty string parts', () => { @@ -38,18 +45,18 @@ describe('onboard/_helpers', () => { const authCode = 'onlyonepart' const result = parseAuthCode(authCode) - expect(result.fingerprintId).toBe('onlyonepart') - expect(result.expiresAt).toBeUndefined() - expect(result.receivedHash).toBeUndefined() + expect(result.fingerprintId).toBe('') + expect(result.expiresAt).toBe('') + expect(result.receivedHash).toBe('') }) test('handles auth code with two parts', () => { const authCode = 'first.second' const result = parseAuthCode(authCode) - expect(result.fingerprintId).toBe('first') - expect(result.expiresAt).toBe('second') - expect(result.receivedHash).toBeUndefined() + expect(result.fingerprintId).toBe('') + expect(result.expiresAt).toBe('') + expect(result.receivedHash).toBe('') }) test('handles empty auth code', () => { @@ -57,8 +64,8 @@ describe('onboard/_helpers', () => { const result = parseAuthCode(authCode) expect(result.fingerprintId).toBe('') - expect(result.expiresAt).toBeUndefined() - expect(result.receivedHash).toBeUndefined() + expect(result.expiresAt).toBe('') + expect(result.receivedHash).toBe('') }) }) diff --git a/web/src/app/onboard/_helpers.ts b/web/src/app/onboard/_helpers.ts index e26a93d67..d502d0d20 100644 --- a/web/src/app/onboard/_helpers.ts +++ b/web/src/app/onboard/_helpers.ts @@ -5,7 +5,24 @@ export function parseAuthCode(authCode: string): { expiresAt: string receivedHash: string } { - const [fingerprintId, expiresAt, receivedHash] = authCode.split('.') + const normalizedAuthCode = authCode.trim() + const hashSeparatorIndex = normalizedAuthCode.lastIndexOf('.') + const expiresSeparatorIndex = normalizedAuthCode.lastIndexOf( + '.', + hashSeparatorIndex - 1, + ) + + if (hashSeparatorIndex === -1 || expiresSeparatorIndex === -1) { + return { fingerprintId: '', expiresAt: '', receivedHash: '' } + } + + const fingerprintId = normalizedAuthCode.slice(0, expiresSeparatorIndex) + const expiresAt = normalizedAuthCode.slice( + expiresSeparatorIndex + 1, + hashSeparatorIndex, + ) + const receivedHash = normalizedAuthCode.slice(hashSeparatorIndex + 1) + return { fingerprintId, expiresAt, receivedHash } }