Skip to content

Commit a68180a

Browse files
committed
Drop invalid Freebuff login auth codes
1 parent 793de91 commit a68180a

5 files changed

Lines changed: 77 additions & 53 deletions

File tree

freebuff/web/src/app/api/auth/[...nextauth]/auth-options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export const authOptions: NextAuthOptions = {
131131
},
132132
'Freebuff auth redirect received non-CLI-shaped auth_code',
133133
)
134+
return baseUrl
134135
}
135136

136137
const onboardUrl = new URL(`${baseUrl}/onboard`)

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ export default async function LoginPage({
2929
const resolvedSearchParams = searchParams ? await searchParams : {}
3030
const rawAuthCode = resolvedSearchParams?.auth_code
3131
const authCode = Array.isArray(rawAuthCode) ? rawAuthCode[0] : rawAuthCode
32+
const validAuthCode =
33+
authCode && isCliAuthCodeCandidate(authCode) ? authCode : undefined
3234
const searchParamKeys = Object.keys(resolvedSearchParams).sort()
3335

3436
if (authCode) {
35-
if (!isCliAuthCodeCandidate(authCode)) {
37+
if (!validAuthCode) {
3638
const headerStore = await headers()
3739
logger.warn(
3840
{
@@ -80,7 +82,9 @@ export default async function LoginPage({
8082
)
8183
}
8284

83-
const { expiresAt } = parseAuthCode(authCode)
85+
const { expiresAt } = validAuthCode
86+
? parseAuthCode(validAuthCode)
87+
: { expiresAt: '' }
8488

8589
if (expiresAt && isAuthCodeExpired(expiresAt)) {
8690
return (
@@ -122,7 +126,7 @@ export default async function LoginPage({
122126
<HeroGrid />
123127
<BackgroundBeams />
124128
<main className="relative z-10 flex flex-col items-center justify-center min-h-screen py-20">
125-
<LoginCard authCode={authCode} />
129+
<LoginCard authCode={validAuthCode} />
126130
</main>
127131
</div>
128132
)

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

Lines changed: 9 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { createHash } from 'node:crypto'
22

33
import { genAuthCode } from '@codebuff/common/util/credentials'
44

5-
const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
6-
const CLI_AUTH_CODE_HASH_RE = /^[a-f0-9]{64}$/i
5+
import {
6+
isCliAuthCodeCandidate,
7+
isOpaqueCliAuthCodeToken,
8+
parseCliAuthCodeShape,
9+
} from '@/lib/cli-auth-code-shape'
10+
11+
export { isCliAuthCodeCandidate, isOpaqueCliAuthCodeToken }
12+
713
const CLI_AUTH_CODE_TOKEN_IDENTIFIER_PREFIX = 'cli-login:'
814
const CONSUMED_CLI_AUTH_CODE_TOKEN_IDENTIFIER_PREFIX = 'cli-login-consumed:'
915
const CONSUMED_CLI_AUTH_CODE_TOKEN_VALUE = 'consumed'
@@ -20,23 +26,6 @@ export function buildCliAuthCode(
2026
return `${fingerprintId}.${expiresAt}.${fingerprintHash}`
2127
}
2228

23-
export function isOpaqueCliAuthCodeToken(authCode: string): boolean {
24-
return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim())
25-
}
26-
27-
export function isCliAuthCodeCandidate(authCode: string): boolean {
28-
if (isOpaqueCliAuthCodeToken(authCode)) {
29-
return true
30-
}
31-
32-
const { fingerprintId, expiresAt, receivedHash } = parseAuthCode(authCode)
33-
return (
34-
fingerprintId.length > 0 &&
35-
/^\d+$/.test(expiresAt) &&
36-
CLI_AUTH_CODE_HASH_RE.test(receivedHash)
37-
)
38-
}
39-
4029
export function getCliAuthCodeHashPrefix(authCode: string): string {
4130
return getCliAuthCodeHash(authCode).slice(0, 12)
4231
}
@@ -123,36 +112,7 @@ export function parseAuthCode(authCode: string): {
123112
expiresAt: string
124113
receivedHash: string
125114
} {
126-
const normalizedAuthCode = authCode.trim()
127-
const hashSeparatorIndex = normalizedAuthCode.lastIndexOf('.')
128-
const expiresSeparatorIndex = normalizedAuthCode.lastIndexOf(
129-
'.',
130-
hashSeparatorIndex - 1,
131-
)
132-
133-
if (hashSeparatorIndex === -1 || expiresSeparatorIndex === -1) {
134-
const legacyMatch = normalizedAuthCode.match(
135-
/^(?<fingerprintId>.+)-(?<expiresAt>\d+)-(?<receivedHash>[a-f0-9]{64})$/i,
136-
)
137-
if (legacyMatch?.groups) {
138-
return {
139-
fingerprintId: legacyMatch.groups.fingerprintId,
140-
expiresAt: legacyMatch.groups.expiresAt,
141-
receivedHash: legacyMatch.groups.receivedHash,
142-
}
143-
}
144-
145-
return { fingerprintId: '', expiresAt: '', receivedHash: '' }
146-
}
147-
148-
const fingerprintId = normalizedAuthCode.slice(0, expiresSeparatorIndex)
149-
const expiresAt = normalizedAuthCode.slice(
150-
expiresSeparatorIndex + 1,
151-
hashSeparatorIndex,
152-
)
153-
const receivedHash = normalizedAuthCode.slice(hashSeparatorIndex + 1)
154-
155-
return { fingerprintId, expiresAt, receivedHash }
115+
return parseCliAuthCodeShape(authCode)
156116
}
157117

158118
export function validateAuthCode(

freebuff/web/src/components/sign-in/sign-in-button.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { useTransition } from 'react'
77
import { Icons } from '../icons'
88
import { Button } from '../ui/button'
99

10+
import { isCliAuthCodeCandidate } from '@/lib/cli-auth-code-shape'
11+
1012
import type { OAuthProviderType } from 'next-auth/providers/oauth-types'
1113

1214
export function SignInButton({
@@ -34,7 +36,7 @@ export function SignInButton({
3436
if (pathname === '/login') {
3537
const authCode = searchParams.get('auth_code')
3638

37-
if (authCode) {
39+
if (authCode && isCliAuthCodeCandidate(authCode)) {
3840
callbackUrl = `/onboard?${searchParams.toString()}`
3941
} else {
4042
callbackUrl = '/'
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const OPAQUE_CLI_AUTH_CODE_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
2+
const CLI_AUTH_CODE_HASH_RE = /^[a-f0-9]{64}$/i
3+
4+
export function isOpaqueCliAuthCodeToken(authCode: string): boolean {
5+
return OPAQUE_CLI_AUTH_CODE_TOKEN_RE.test(authCode.trim())
6+
}
7+
8+
export function parseCliAuthCodeShape(authCode: string): {
9+
fingerprintId: string
10+
expiresAt: string
11+
receivedHash: string
12+
} {
13+
const normalizedAuthCode = authCode.trim()
14+
const hashSeparatorIndex = normalizedAuthCode.lastIndexOf('.')
15+
const expiresSeparatorIndex = normalizedAuthCode.lastIndexOf(
16+
'.',
17+
hashSeparatorIndex - 1,
18+
)
19+
20+
if (hashSeparatorIndex === -1 || expiresSeparatorIndex === -1) {
21+
const legacyMatch = normalizedAuthCode.match(
22+
/^(?<fingerprintId>.+)-(?<expiresAt>\d+)-(?<receivedHash>[a-f0-9]{64})$/i,
23+
)
24+
if (legacyMatch?.groups) {
25+
return {
26+
fingerprintId: legacyMatch.groups.fingerprintId,
27+
expiresAt: legacyMatch.groups.expiresAt,
28+
receivedHash: legacyMatch.groups.receivedHash,
29+
}
30+
}
31+
32+
return { fingerprintId: '', expiresAt: '', receivedHash: '' }
33+
}
34+
35+
const fingerprintId = normalizedAuthCode.slice(0, expiresSeparatorIndex)
36+
const expiresAt = normalizedAuthCode.slice(
37+
expiresSeparatorIndex + 1,
38+
hashSeparatorIndex,
39+
)
40+
const receivedHash = normalizedAuthCode.slice(hashSeparatorIndex + 1)
41+
42+
return { fingerprintId, expiresAt, receivedHash }
43+
}
44+
45+
export function isCliAuthCodeCandidate(authCode: string): boolean {
46+
if (isOpaqueCliAuthCodeToken(authCode)) {
47+
return true
48+
}
49+
50+
const { fingerprintId, expiresAt, receivedHash } =
51+
parseCliAuthCodeShape(authCode)
52+
return (
53+
fingerprintId.length > 0 &&
54+
/^\d+$/.test(expiresAt) &&
55+
CLI_AUTH_CODE_HASH_RE.test(receivedHash)
56+
)
57+
}

0 commit comments

Comments
 (0)