From 9abc454ee72cf8ba1172a1baf13d4c81e6fa320b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 8 May 2026 16:11:30 -0700 Subject: [PATCH] Use opaque CLI auth tokens --- freebuff/web/src/app/api/auth/cli/code/route.ts | 16 ++++++++++++---- freebuff/web/src/app/onboard/_db.ts | 17 +++++++++++++++++ freebuff/web/src/app/onboard/page.tsx | 7 ++++++- web/src/app/api/auth/cli/code/route.ts | 16 ++++++++++++---- web/src/app/onboard/_db.ts | 17 +++++++++++++++++ web/src/app/onboard/page.tsx | 5 ++++- 6 files changed, 68 insertions(+), 10 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 dfd77dca2..315284d95 100644 --- a/freebuff/web/src/app/api/auth/cli/code/route.ts +++ b/freebuff/web/src/app/api/auth/cli/code/route.ts @@ -1,3 +1,5 @@ +import { randomBytes } from 'node:crypto' + import { genAuthCode } from '@codebuff/common/util/credentials' import db from '@codebuff/internal/db' import * as schema from '@codebuff/internal/db/schema' @@ -55,6 +57,15 @@ export async function POST(req: Request) { ) } + const authCode = `${fingerprintId}.${expiresAt}.${fingerprintHash}` + const loginToken = randomBytes(32).toString('base64url') + + await db.insert(schema.verificationToken).values({ + identifier: `cli-login:${loginToken}`, + token: authCode, + expires: new Date(expiresAt), + }) + const loginUrl = new URL( '/login', getLoginUrlOrigin( @@ -64,10 +75,7 @@ export async function POST(req: Request) { env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod', ), ) - loginUrl.searchParams.set( - 'auth_code', - `${fingerprintId}.${expiresAt}.${fingerprintHash}`, - ) + loginUrl.searchParams.set('auth_code', loginToken) return NextResponse.json({ fingerprintId, diff --git a/freebuff/web/src/app/onboard/_db.ts b/freebuff/web/src/app/onboard/_db.ts index 078d757d5..0e3858798 100644 --- a/freebuff/web/src/app/onboard/_db.ts +++ b/freebuff/web/src/app/onboard/_db.ts @@ -32,6 +32,23 @@ export async function hasCliSessionForAuthHash( return existing.length > 0 } +export async function getCliAuthCodeForToken( + authCodeToken: string, +): Promise { + const existing = await db + .select({ authCode: schema.verificationToken.token }) + .from(schema.verificationToken) + .where( + and( + eq(schema.verificationToken.identifier, `cli-login:${authCodeToken}`), + gt(schema.verificationToken.expires, new Date()), + ), + ) + .limit(1) + + return existing[0]?.authCode ?? null +} + export async function checkFingerprintConflict( fingerprintId: string, userId: string, diff --git a/freebuff/web/src/app/onboard/page.tsx b/freebuff/web/src/app/onboard/page.tsx index 180758a23..21f6e6135 100644 --- a/freebuff/web/src/app/onboard/page.tsx +++ b/freebuff/web/src/app/onboard/page.tsx @@ -7,6 +7,7 @@ import { getServerSession } from 'next-auth' import { checkFingerprintConflict, createCliSession, + getCliAuthCodeForToken, getSessionTokenFromCookies, hasCliSessionForAuthHash, } from './_db' @@ -91,7 +92,9 @@ const Onboard = async ({ searchParams }: PageProps) => { ) } - const { fingerprintId, expiresAt, receivedHash } = parseAuthCode(authCode) + const resolvedAuthCode = (await getCliAuthCodeForToken(authCode)) ?? authCode + const { fingerprintId, expiresAt, receivedHash } = + parseAuthCode(resolvedAuthCode) const { valid, expectedHash: fingerprintHash } = validateAuthCode( receivedHash, fingerprintId, @@ -103,6 +106,8 @@ const Onboard = async ({ searchParams }: PageProps) => { logger.warn( { authCodeLength: authCode.length, + resolvedAuthCode: resolvedAuthCode !== authCode, + resolvedAuthCodeLength: resolvedAuthCode.length, 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 993a82154..455375d60 100644 --- a/web/src/app/api/auth/cli/code/route.ts +++ b/web/src/app/api/auth/cli/code/route.ts @@ -1,3 +1,5 @@ +import { randomBytes } from 'node:crypto' + import { genAuthCode } from '@codebuff/common/util/credentials' import db from '@codebuff/internal/db' import * as schema from '@codebuff/internal/db/schema' @@ -57,6 +59,15 @@ export async function POST(req: Request) { ) } + const authCode = `${fingerprintId}.${expiresAt}.${fingerprintHash}` + const loginToken = randomBytes(32).toString('base64url') + + await db.insert(schema.verificationToken).values({ + identifier: `cli-login:${loginToken}`, + token: authCode, + expires: new Date(expiresAt), + }) + const loginUrl = new URL( '/login', getLoginUrlOrigin( @@ -66,10 +77,7 @@ export async function POST(req: Request) { env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod', ), ) - loginUrl.searchParams.set( - 'auth_code', - `${fingerprintId}.${expiresAt}.${fingerprintHash}`, - ) + loginUrl.searchParams.set('auth_code', loginToken) return NextResponse.json({ fingerprintId, diff --git a/web/src/app/onboard/_db.ts b/web/src/app/onboard/_db.ts index 078d757d5..0e3858798 100644 --- a/web/src/app/onboard/_db.ts +++ b/web/src/app/onboard/_db.ts @@ -32,6 +32,23 @@ export async function hasCliSessionForAuthHash( return existing.length > 0 } +export async function getCliAuthCodeForToken( + authCodeToken: string, +): Promise { + const existing = await db + .select({ authCode: schema.verificationToken.token }) + .from(schema.verificationToken) + .where( + and( + eq(schema.verificationToken.identifier, `cli-login:${authCodeToken}`), + gt(schema.verificationToken.expires, new Date()), + ), + ) + .limit(1) + + return existing[0]?.authCode ?? null +} + export async function checkFingerprintConflict( fingerprintId: string, userId: string, diff --git a/web/src/app/onboard/page.tsx b/web/src/app/onboard/page.tsx index 6e5ea8f88..aba3ded26 100644 --- a/web/src/app/onboard/page.tsx +++ b/web/src/app/onboard/page.tsx @@ -7,6 +7,7 @@ import { getServerSession } from 'next-auth' import { checkFingerprintConflict, createCliSession, + getCliAuthCodeForToken, getSessionTokenFromCookies, hasCliSessionForAuthHash, } from './_db' @@ -48,7 +49,9 @@ const Onboard = async ({ searchParams }: PageProps) => { ) } - const { fingerprintId, expiresAt, receivedHash } = parseAuthCode(authCode) + const resolvedAuthCode = (await getCliAuthCodeForToken(authCode)) ?? authCode + const { fingerprintId, expiresAt, receivedHash } = + parseAuthCode(resolvedAuthCode) const { valid, expectedHash: fingerprintHash } = validateAuthCode( receivedHash, fingerprintId,