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 6622af094..36ca660e4 100644 --- a/freebuff/web/src/app/api/auth/cli/code/route.ts +++ b/freebuff/web/src/app/api/auth/cli/code/route.ts @@ -8,7 +8,10 @@ import { and, eq, gt } from 'drizzle-orm' import { NextResponse } from 'next/server' import { z } from 'zod/v4' -import { buildCliAuthCode } from '@/app/onboard/_helpers' +import { + buildCliAuthCode, + getCliAuthCodeHashPrefix, +} from '@/app/onboard/_helpers' import { logger } from '@/util/logger' import { getLoginUrlOrigin } from './_origin' @@ -82,6 +85,25 @@ export async function POST(req: Request) { ) loginUrl.searchParams.set('auth_code', loginToken) + logger.info( + { + authCodeTokenHashPrefix: getCliAuthCodeHashPrefix(loginToken), + authCodeTokenLength: loginToken.length, + fingerprintIdPrefix: fingerprintId.slice(0, 24), + fingerprintIdLength: fingerprintId.length, + expiresAt, + loginUrlOrigin: loginUrl.origin, + requestOrigin: new URL(req.url).origin, + requestHost: req.headers.get('host'), + forwardedHost: req.headers.get('x-forwarded-host'), + forwardedProto: req.headers.get('x-forwarded-proto'), + originHeader: req.headers.get('origin'), + configuredAppUrl: env.NEXT_PUBLIC_CODEBUFF_APP_URL, + environment: env.NEXT_PUBLIC_CB_ENVIRONMENT, + }, + 'Issued Freebuff CLI auth code token', + ) + return NextResponse.json({ fingerprintId, fingerprintHash, diff --git a/freebuff/web/src/app/onboard/__tests__/helpers.test.ts b/freebuff/web/src/app/onboard/__tests__/helpers.test.ts index 0a19061b8..a1b6462b5 100644 --- a/freebuff/web/src/app/onboard/__tests__/helpers.test.ts +++ b/freebuff/web/src/app/onboard/__tests__/helpers.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' import { buildCliAuthCode, + getCliAuthCodeHashPrefix, isAuthCodeExpired, isOpaqueCliAuthCodeToken, parseAuthCode, @@ -110,6 +111,13 @@ describe('freebuff onboard/_helpers', () => { expect(isOpaqueCliAuthCodeToken(`${'A'.repeat(42)}.`)).toBe(false) }) + test('hashes auth codes for log correlation without logging the token', () => { + expect(getCliAuthCodeHashPrefix('a'.repeat(43))).toBe('66d34fba71f8') + expect(getCliAuthCodeHashPrefix(` ${'a'.repeat(43)}\n`)).toBe( + '66d34fba71f8', + ) + }) + test('resolves an opaque browser token before validation', async () => { const expiresAt = '4102444800000' const fingerprintHash = genAuthCode( diff --git a/freebuff/web/src/app/onboard/_helpers.ts b/freebuff/web/src/app/onboard/_helpers.ts index a3daf585a..54979932a 100644 --- a/freebuff/web/src/app/onboard/_helpers.ts +++ b/freebuff/web/src/app/onboard/_helpers.ts @@ -1,3 +1,5 @@ +import { createHash } from 'node:crypto' + import { genAuthCode } from '@codebuff/common/util/credentials' const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/ @@ -14,6 +16,10 @@ export function isOpaqueCliAuthCodeToken(authCode: string): boolean { return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim()) } +export function getCliAuthCodeHashPrefix(authCode: string): string { + return createHash('sha256').update(authCode.trim()).digest('hex').slice(0, 12) +} + export async function resolveCliAuthCode( authCode: string, consumeCliAuthCodeToken: (authCodeToken: string) => Promise, diff --git a/freebuff/web/src/app/onboard/page.tsx b/freebuff/web/src/app/onboard/page.tsx index e39a4a0b3..63cb7c31d 100644 --- a/freebuff/web/src/app/onboard/page.tsx +++ b/freebuff/web/src/app/onboard/page.tsx @@ -12,7 +12,9 @@ import { hasCliSessionForAuthHash, } from './_db' import { + getCliAuthCodeHashPrefix, isAuthCodeExpired, + isOpaqueCliAuthCodeToken, parseAuthCode, resolveCliAuthCode, validateAuthCode, @@ -112,8 +114,12 @@ const Onboard = async ({ searchParams }: PageProps) => { logger.warn( { authCodeLength: authCode.length, + authCodeTrimmedLength: authCode.trim().length, + authCodeHashPrefix: getCliAuthCodeHashPrefix(authCode), + isOpaqueAuthCodeToken: isOpaqueCliAuthCodeToken(authCode), resolvedAuthCode: resolvedOpaqueToken, resolvedAuthCodeLength: resolvedAuthCode.length, + userId: user.id, dotCount: authCode.match(/\./g)?.length ?? 0, hyphenCount: authCode.match(/-/g)?.length ?? 0, fingerprintIdPrefix: fingerprintId.slice(0, 24), diff --git a/web/src/app/api/auth/cli/code/route.ts b/web/src/app/api/auth/cli/code/route.ts index 1149a46de..a9a82a835 100644 --- a/web/src/app/api/auth/cli/code/route.ts +++ b/web/src/app/api/auth/cli/code/route.ts @@ -8,7 +8,10 @@ import { and, eq, gt } from 'drizzle-orm' import { NextResponse } from 'next/server' import { z } from 'zod/v4' -import { buildCliAuthCode } from '@/app/onboard/_helpers' +import { + buildCliAuthCode, + getCliAuthCodeHashPrefix, +} from '@/app/onboard/_helpers' import { logger } from '@/util/logger' import { getLoginUrlOrigin } from './_origin' @@ -84,6 +87,25 @@ export async function POST(req: Request) { ) loginUrl.searchParams.set('auth_code', loginToken) + logger.info( + { + authCodeTokenHashPrefix: getCliAuthCodeHashPrefix(loginToken), + authCodeTokenLength: loginToken.length, + fingerprintIdPrefix: fingerprintId.slice(0, 24), + fingerprintIdLength: fingerprintId.length, + expiresAt, + loginUrlOrigin: loginUrl.origin, + requestOrigin: new URL(req.url).origin, + requestHost: req.headers.get('host'), + forwardedHost: req.headers.get('x-forwarded-host'), + forwardedProto: req.headers.get('x-forwarded-proto'), + originHeader: req.headers.get('origin'), + configuredAppUrl: env.NEXT_PUBLIC_CODEBUFF_APP_URL, + environment: env.NEXT_PUBLIC_CB_ENVIRONMENT, + }, + 'Issued Codebuff CLI auth code token', + ) + return NextResponse.json({ fingerprintId, fingerprintHash, diff --git a/web/src/app/onboard/__tests__/helpers.test.ts b/web/src/app/onboard/__tests__/helpers.test.ts index c47c2f642..767bd4684 100644 --- a/web/src/app/onboard/__tests__/helpers.test.ts +++ b/web/src/app/onboard/__tests__/helpers.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' import { buildCliAuthCode, + getCliAuthCodeHashPrefix, isAuthCodeExpired, isOpaqueCliAuthCodeToken, parseAuthCode, @@ -238,6 +239,13 @@ describe('onboard/_helpers', () => { expect(isOpaqueCliAuthCodeToken(`${'A'.repeat(42)}.`)).toBe(false) }) + test('hashes auth codes for log correlation without logging the token', () => { + expect(getCliAuthCodeHashPrefix('a'.repeat(43))).toBe('66d34fba71f8') + expect(getCliAuthCodeHashPrefix(` ${'a'.repeat(43)}\n`)).toBe( + '66d34fba71f8', + ) + }) + test('resolves an opaque browser token before validation', async () => { const expiresAt = '4102444800000' const fingerprintHash = genAuthCode( diff --git a/web/src/app/onboard/_helpers.ts b/web/src/app/onboard/_helpers.ts index a3daf585a..54979932a 100644 --- a/web/src/app/onboard/_helpers.ts +++ b/web/src/app/onboard/_helpers.ts @@ -1,3 +1,5 @@ +import { createHash } from 'node:crypto' + import { genAuthCode } from '@codebuff/common/util/credentials' const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/ @@ -14,6 +16,10 @@ export function isOpaqueCliAuthCodeToken(authCode: string): boolean { return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim()) } +export function getCliAuthCodeHashPrefix(authCode: string): string { + return createHash('sha256').update(authCode.trim()).digest('hex').slice(0, 12) +} + export async function resolveCliAuthCode( authCode: string, consumeCliAuthCodeToken: (authCodeToken: string) => Promise,