Skip to content

Commit 77ca87c

Browse files
authored
Log CLI auth token correlation fields (#631)
1 parent 43d0008 commit 77ca87c

7 files changed

Lines changed: 80 additions & 2 deletions

File tree

freebuff/web/src/app/api/auth/cli/code/route.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { and, eq, gt } from 'drizzle-orm'
88
import { NextResponse } from 'next/server'
99
import { z } from 'zod/v4'
1010

11-
import { buildCliAuthCode } from '@/app/onboard/_helpers'
11+
import {
12+
buildCliAuthCode,
13+
getCliAuthCodeHashPrefix,
14+
} from '@/app/onboard/_helpers'
1215
import { logger } from '@/util/logger'
1316

1417
import { getLoginUrlOrigin } from './_origin'
@@ -82,6 +85,25 @@ export async function POST(req: Request) {
8285
)
8386
loginUrl.searchParams.set('auth_code', loginToken)
8487

88+
logger.info(
89+
{
90+
authCodeTokenHashPrefix: getCliAuthCodeHashPrefix(loginToken),
91+
authCodeTokenLength: loginToken.length,
92+
fingerprintIdPrefix: fingerprintId.slice(0, 24),
93+
fingerprintIdLength: fingerprintId.length,
94+
expiresAt,
95+
loginUrlOrigin: loginUrl.origin,
96+
requestOrigin: new URL(req.url).origin,
97+
requestHost: req.headers.get('host'),
98+
forwardedHost: req.headers.get('x-forwarded-host'),
99+
forwardedProto: req.headers.get('x-forwarded-proto'),
100+
originHeader: req.headers.get('origin'),
101+
configuredAppUrl: env.NEXT_PUBLIC_CODEBUFF_APP_URL,
102+
environment: env.NEXT_PUBLIC_CB_ENVIRONMENT,
103+
},
104+
'Issued Freebuff CLI auth code token',
105+
)
106+
85107
return NextResponse.json({
86108
fingerprintId,
87109
fingerprintHash,

freebuff/web/src/app/onboard/__tests__/helpers.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
33

44
import {
55
buildCliAuthCode,
6+
getCliAuthCodeHashPrefix,
67
isAuthCodeExpired,
78
isOpaqueCliAuthCodeToken,
89
parseAuthCode,
@@ -110,6 +111,13 @@ describe('freebuff onboard/_helpers', () => {
110111
expect(isOpaqueCliAuthCodeToken(`${'A'.repeat(42)}.`)).toBe(false)
111112
})
112113

114+
test('hashes auth codes for log correlation without logging the token', () => {
115+
expect(getCliAuthCodeHashPrefix('a'.repeat(43))).toBe('66d34fba71f8')
116+
expect(getCliAuthCodeHashPrefix(` ${'a'.repeat(43)}\n`)).toBe(
117+
'66d34fba71f8',
118+
)
119+
})
120+
113121
test('resolves an opaque browser token before validation', async () => {
114122
const expiresAt = '4102444800000'
115123
const fingerprintHash = genAuthCode(

freebuff/web/src/app/onboard/_helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createHash } from 'node:crypto'
2+
13
import { genAuthCode } from '@codebuff/common/util/credentials'
24

35
const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
@@ -14,6 +16,10 @@ export function isOpaqueCliAuthCodeToken(authCode: string): boolean {
1416
return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim())
1517
}
1618

19+
export function getCliAuthCodeHashPrefix(authCode: string): string {
20+
return createHash('sha256').update(authCode.trim()).digest('hex').slice(0, 12)
21+
}
22+
1723
export async function resolveCliAuthCode(
1824
authCode: string,
1925
consumeCliAuthCodeToken: (authCodeToken: string) => Promise<string | null>,

freebuff/web/src/app/onboard/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
hasCliSessionForAuthHash,
1313
} from './_db'
1414
import {
15+
getCliAuthCodeHashPrefix,
1516
isAuthCodeExpired,
17+
isOpaqueCliAuthCodeToken,
1618
parseAuthCode,
1719
resolveCliAuthCode,
1820
validateAuthCode,
@@ -112,8 +114,12 @@ const Onboard = async ({ searchParams }: PageProps) => {
112114
logger.warn(
113115
{
114116
authCodeLength: authCode.length,
117+
authCodeTrimmedLength: authCode.trim().length,
118+
authCodeHashPrefix: getCliAuthCodeHashPrefix(authCode),
119+
isOpaqueAuthCodeToken: isOpaqueCliAuthCodeToken(authCode),
115120
resolvedAuthCode: resolvedOpaqueToken,
116121
resolvedAuthCodeLength: resolvedAuthCode.length,
122+
userId: user.id,
117123
dotCount: authCode.match(/\./g)?.length ?? 0,
118124
hyphenCount: authCode.match(/-/g)?.length ?? 0,
119125
fingerprintIdPrefix: fingerprintId.slice(0, 24),

web/src/app/api/auth/cli/code/route.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { and, eq, gt } from 'drizzle-orm'
88
import { NextResponse } from 'next/server'
99
import { z } from 'zod/v4'
1010

11-
import { buildCliAuthCode } from '@/app/onboard/_helpers'
11+
import {
12+
buildCliAuthCode,
13+
getCliAuthCodeHashPrefix,
14+
} from '@/app/onboard/_helpers'
1215
import { logger } from '@/util/logger'
1316

1417
import { getLoginUrlOrigin } from './_origin'
@@ -84,6 +87,25 @@ export async function POST(req: Request) {
8487
)
8588
loginUrl.searchParams.set('auth_code', loginToken)
8689

90+
logger.info(
91+
{
92+
authCodeTokenHashPrefix: getCliAuthCodeHashPrefix(loginToken),
93+
authCodeTokenLength: loginToken.length,
94+
fingerprintIdPrefix: fingerprintId.slice(0, 24),
95+
fingerprintIdLength: fingerprintId.length,
96+
expiresAt,
97+
loginUrlOrigin: loginUrl.origin,
98+
requestOrigin: new URL(req.url).origin,
99+
requestHost: req.headers.get('host'),
100+
forwardedHost: req.headers.get('x-forwarded-host'),
101+
forwardedProto: req.headers.get('x-forwarded-proto'),
102+
originHeader: req.headers.get('origin'),
103+
configuredAppUrl: env.NEXT_PUBLIC_CODEBUFF_APP_URL,
104+
environment: env.NEXT_PUBLIC_CB_ENVIRONMENT,
105+
},
106+
'Issued Codebuff CLI auth code token',
107+
)
108+
87109
return NextResponse.json({
88110
fingerprintId,
89111
fingerprintHash,

web/src/app/onboard/__tests__/helpers.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
33

44
import {
55
buildCliAuthCode,
6+
getCliAuthCodeHashPrefix,
67
isAuthCodeExpired,
78
isOpaqueCliAuthCodeToken,
89
parseAuthCode,
@@ -238,6 +239,13 @@ describe('onboard/_helpers', () => {
238239
expect(isOpaqueCliAuthCodeToken(`${'A'.repeat(42)}.`)).toBe(false)
239240
})
240241

242+
test('hashes auth codes for log correlation without logging the token', () => {
243+
expect(getCliAuthCodeHashPrefix('a'.repeat(43))).toBe('66d34fba71f8')
244+
expect(getCliAuthCodeHashPrefix(` ${'a'.repeat(43)}\n`)).toBe(
245+
'66d34fba71f8',
246+
)
247+
})
248+
241249
test('resolves an opaque browser token before validation', async () => {
242250
const expiresAt = '4102444800000'
243251
const fingerprintHash = genAuthCode(

web/src/app/onboard/_helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createHash } from 'node:crypto'
2+
13
import { genAuthCode } from '@codebuff/common/util/credentials'
24

35
const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
@@ -14,6 +16,10 @@ export function isOpaqueCliAuthCodeToken(authCode: string): boolean {
1416
return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim())
1517
}
1618

19+
export function getCliAuthCodeHashPrefix(authCode: string): string {
20+
return createHash('sha256').update(authCode.trim()).digest('hex').slice(0, 12)
21+
}
22+
1723
export async function resolveCliAuthCode(
1824
authCode: string,
1925
consumeCliAuthCodeToken: (authCodeToken: string) => Promise<string | null>,

0 commit comments

Comments
 (0)