diff --git a/freebuff/web/src/app/api/auth/[...nextauth]/auth-options.ts b/freebuff/web/src/app/api/auth/[...nextauth]/auth-options.ts
index b2b4467578..53a0d05aea 100644
--- a/freebuff/web/src/app/api/auth/[...nextauth]/auth-options.ts
+++ b/freebuff/web/src/app/api/auth/[...nextauth]/auth-options.ts
@@ -17,6 +17,7 @@ import type { Adapter } from 'next-auth/adapters'
import {
getCliAuthCodeHashPrefix,
+ getCliAuthOnboardSearchParams,
isCliAuthCodeCandidate,
} from '@/app/onboard/_helpers'
import { logger } from '@/util/logger'
@@ -131,12 +132,14 @@ export const authOptions: NextAuthOptions = {
},
'Freebuff auth redirect received non-CLI-shaped auth_code',
)
+ return baseUrl
}
const onboardUrl = new URL(`${baseUrl}/onboard`)
- potentialRedirectUrl.searchParams.forEach((value, key) => {
- onboardUrl.searchParams.set(key, value)
- })
+ onboardUrl.search = getCliAuthOnboardSearchParams(
+ potentialRedirectUrl.searchParams,
+ authCode,
+ ).toString()
return onboardUrl.toString()
}
diff --git a/freebuff/web/src/app/login/page.tsx b/freebuff/web/src/app/login/page.tsx
index 311cc2931d..6dd45aca90 100644
--- a/freebuff/web/src/app/login/page.tsx
+++ b/freebuff/web/src/app/login/page.tsx
@@ -29,10 +29,12 @@ export default async function LoginPage({
const resolvedSearchParams = searchParams ? await searchParams : {}
const rawAuthCode = resolvedSearchParams?.auth_code
const authCode = Array.isArray(rawAuthCode) ? rawAuthCode[0] : rawAuthCode
+ const validAuthCode =
+ authCode && isCliAuthCodeCandidate(authCode) ? authCode : undefined
const searchParamKeys = Object.keys(resolvedSearchParams).sort()
if (authCode) {
- if (!isCliAuthCodeCandidate(authCode)) {
+ if (!validAuthCode) {
const headerStore = await headers()
logger.warn(
{
@@ -80,7 +82,9 @@ export default async function LoginPage({
)
}
- const { expiresAt } = parseAuthCode(authCode)
+ const { expiresAt } = validAuthCode
+ ? parseAuthCode(validAuthCode)
+ : { expiresAt: '' }
if (expiresAt && isAuthCodeExpired(expiresAt)) {
return (
@@ -122,7 +126,7 @@ export default async function LoginPage({
-
+
)
diff --git a/freebuff/web/src/app/onboard/_helpers.ts b/freebuff/web/src/app/onboard/_helpers.ts
index 35901fb112..53823389be 100644
--- a/freebuff/web/src/app/onboard/_helpers.ts
+++ b/freebuff/web/src/app/onboard/_helpers.ts
@@ -2,8 +2,19 @@ import { createHash } from 'node:crypto'
import { genAuthCode } from '@codebuff/common/util/credentials'
-const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
-const CLI_AUTH_CODE_HASH_RE = /^[a-f0-9]{64}$/i
+import {
+ getCliAuthOnboardSearchParams,
+ isCliAuthCodeCandidate,
+ isOpaqueCliAuthCodeToken,
+ parseCliAuthCodeShape,
+} from '@/lib/cli-auth-code-shape'
+
+export {
+ getCliAuthOnboardSearchParams,
+ isCliAuthCodeCandidate,
+ isOpaqueCliAuthCodeToken,
+}
+
const CLI_AUTH_CODE_TOKEN_IDENTIFIER_PREFIX = 'cli-login:'
const CONSUMED_CLI_AUTH_CODE_TOKEN_IDENTIFIER_PREFIX = 'cli-login-consumed:'
const CONSUMED_CLI_AUTH_CODE_TOKEN_VALUE = 'consumed'
@@ -20,23 +31,6 @@ export function buildCliAuthCode(
return `${fingerprintId}.${expiresAt}.${fingerprintHash}`
}
-export function isOpaqueCliAuthCodeToken(authCode: string): boolean {
- return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim())
-}
-
-export function isCliAuthCodeCandidate(authCode: string): boolean {
- if (isOpaqueCliAuthCodeToken(authCode)) {
- return true
- }
-
- const { fingerprintId, expiresAt, receivedHash } = parseAuthCode(authCode)
- return (
- fingerprintId.length > 0 &&
- /^\d+$/.test(expiresAt) &&
- CLI_AUTH_CODE_HASH_RE.test(receivedHash)
- )
-}
-
export function getCliAuthCodeHashPrefix(authCode: string): string {
return getCliAuthCodeHash(authCode).slice(0, 12)
}
@@ -123,36 +117,7 @@ export function parseAuthCode(authCode: string): {
expiresAt: string
receivedHash: string
} {
- const normalizedAuthCode = authCode.trim()
- const hashSeparatorIndex = normalizedAuthCode.lastIndexOf('.')
- const expiresSeparatorIndex = normalizedAuthCode.lastIndexOf(
- '.',
- hashSeparatorIndex - 1,
- )
-
- if (hashSeparatorIndex === -1 || expiresSeparatorIndex === -1) {
- const legacyMatch = normalizedAuthCode.match(
- /^(?.+)-(?\d+)-(?[a-f0-9]{64})$/i,
- )
- if (legacyMatch?.groups) {
- return {
- fingerprintId: legacyMatch.groups.fingerprintId,
- expiresAt: legacyMatch.groups.expiresAt,
- receivedHash: legacyMatch.groups.receivedHash,
- }
- }
-
- 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 }
+ return parseCliAuthCodeShape(authCode)
}
export function validateAuthCode(
diff --git a/freebuff/web/src/components/login/login-card.tsx b/freebuff/web/src/components/login/login-card.tsx
index c1338f4325..104045932e 100644
--- a/freebuff/web/src/components/login/login-card.tsx
+++ b/freebuff/web/src/components/login/login-card.tsx
@@ -14,6 +14,7 @@ import {
CardContent,
CardFooter,
} from '@/components/ui/card'
+import { getCliAuthOnboardPath } from '@/lib/cli-auth-code-shape'
export function LoginCard({ authCode }: { authCode?: string | null }) {
const { data: session } = useSession()
@@ -32,7 +33,7 @@ export function LoginCard({ authCode }: { authCode?: string | null }) {
let callbackUrl = '/'
if (authCode) {
- callbackUrl = `/onboard?${searchParams.toString()}`
+ callbackUrl = getCliAuthOnboardPath(searchParams, authCode)
}
window.location.href = callbackUrl
@@ -41,11 +42,9 @@ export function LoginCard({ authCode }: { authCode?: string | null }) {
const handleUseAnotherAccount = () => {
persistReferrer()
- const searchParamsString = searchParams.toString()
-
let callbackUrl = '/login'
if (authCode) {
- callbackUrl = `/onboard?${searchParamsString}`
+ callbackUrl = getCliAuthOnboardPath(searchParams, authCode)
}
signIn('github', { callbackUrl, prompt: 'login' })
diff --git a/freebuff/web/src/components/sign-in/sign-in-button.tsx b/freebuff/web/src/components/sign-in/sign-in-button.tsx
index 66fb41fb82..7d7725f499 100644
--- a/freebuff/web/src/components/sign-in/sign-in-button.tsx
+++ b/freebuff/web/src/components/sign-in/sign-in-button.tsx
@@ -7,6 +7,11 @@ import { useTransition } from 'react'
import { Icons } from '../icons'
import { Button } from '../ui/button'
+import {
+ getCliAuthOnboardPath,
+ isCliAuthCodeCandidate,
+} from '@/lib/cli-auth-code-shape'
+
import type { OAuthProviderType } from 'next-auth/providers/oauth-types'
export function SignInButton({
@@ -34,8 +39,8 @@ export function SignInButton({
if (pathname === '/login') {
const authCode = searchParams.get('auth_code')
- if (authCode) {
- callbackUrl = `/onboard?${searchParams.toString()}`
+ if (authCode && isCliAuthCodeCandidate(authCode)) {
+ callbackUrl = getCliAuthOnboardPath(searchParams, authCode)
} else {
callbackUrl = '/'
}
diff --git a/freebuff/web/src/lib/cli-auth-code-shape.ts b/freebuff/web/src/lib/cli-auth-code-shape.ts
new file mode 100644
index 0000000000..00436dee09
--- /dev/null
+++ b/freebuff/web/src/lib/cli-auth-code-shape.ts
@@ -0,0 +1,81 @@
+const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
+const CLI_AUTH_CODE_HASH_RE = /^[a-f0-9]{64}$/i
+
+export function isOpaqueCliAuthCodeToken(authCode: string): boolean {
+ return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim())
+}
+
+export function parseCliAuthCodeShape(authCode: string): {
+ fingerprintId: string
+ expiresAt: string
+ receivedHash: string
+} {
+ const normalizedAuthCode = authCode.trim()
+ const hashSeparatorIndex = normalizedAuthCode.lastIndexOf('.')
+ const expiresSeparatorIndex = normalizedAuthCode.lastIndexOf(
+ '.',
+ hashSeparatorIndex - 1,
+ )
+
+ if (hashSeparatorIndex === -1 || expiresSeparatorIndex === -1) {
+ const legacyMatch = normalizedAuthCode.match(
+ /^(?.+)-(?\d+)-(?[a-f0-9]{64})$/i,
+ )
+ if (legacyMatch?.groups) {
+ return {
+ fingerprintId: legacyMatch.groups.fingerprintId,
+ expiresAt: legacyMatch.groups.expiresAt,
+ receivedHash: legacyMatch.groups.receivedHash,
+ }
+ }
+
+ 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 }
+}
+
+export function isCliAuthCodeCandidate(authCode: string): boolean {
+ if (isOpaqueCliAuthCodeToken(authCode)) {
+ return true
+ }
+
+ const { fingerprintId, expiresAt, receivedHash } =
+ parseCliAuthCodeShape(authCode)
+ return (
+ fingerprintId.length > 0 &&
+ /^\d+$/.test(expiresAt) &&
+ CLI_AUTH_CODE_HASH_RE.test(receivedHash)
+ )
+}
+
+export function getCliAuthOnboardSearchParams(
+ searchParams: URLSearchParams,
+ authCode: string,
+): URLSearchParams {
+ const onboardParams = new URLSearchParams()
+ searchParams.forEach((value, key) => {
+ if (key !== 'auth_code') {
+ onboardParams.append(key, value)
+ }
+ })
+ onboardParams.set('auth_code', authCode)
+ return onboardParams
+}
+
+export function getCliAuthOnboardPath(
+ searchParams: URLSearchParams,
+ authCode: string,
+): string {
+ return `/onboard?${getCliAuthOnboardSearchParams(
+ searchParams,
+ authCode,
+ ).toString()}`
+}