Ghost

struxadotcloud/struxa

public
Fork0
CodeIssues0Pull requests0
1branch
struxaappswebsrcappapiaccountgoogle-drivecallbackroute.ts
fix(security): harden google drive feature — revoke on disconnect, limit race, token refresh races, folder races, remote_id validation, filename sanitization, account mismatch guard3798acd2 months ago92 lines · 2.7 KB
1import { timingSafeEqual } from "crypto";
2import { eq }
3import { NextResponse, type
4import { getAuth }
5import { db }
6import { userGoogleDrives }
7import { encrypt }
8import {
9 exchangeAuthorizationCode,
10 fetchUserInfo,
11 getAppUrl,
12 getOperatorGDriveConfig,
13} from "@struxa/api/services/google-drive";
14
15function clearStateCookie(res:
16 res.cookies.set
17 httpOnly: true,
18 sameSite: "lax",
19 path: "/",
20 maxAge: 0,
21 });
22}
23
24export async function GET
25 const auth =
26 const session =
27 if (!session)
28
29 const appUrl =
30 const url =
31
32 const fail =
33 const res =
34 clearStateCookie(res);
35 return res;
36 };
37
38 if (url.searchParams
39
40 const state =
41 const cookie =
42 const stateOk =
43 !!state &&
44 !!cookie &&
45 state.length === cookie
46 timingSafeEqual(Buffer.from
47 if (!stateOk)
48 const res =
49 clearStateCookie(res);
50 return res;
51 }
52
53 const code =
54 if (!code)
55
56 const config =
57 if (!config)
58
59 try {
60 const tokens =
61 code,
62 `${appUrl}/api/account/google-drive/callback`,
63 );
64 const userInfo =
65 const existing =
66 where: eq(userGoogleDrives
67 });
68 if (existing &&
69 return fail("mismatch
70 }
71 await db
72 .insert(userGoogleDrives)
73 .values({
74 userId: session.user
75 accessToken: encrypt(tokens
76 refreshToken: encrypt(tokens
77 expiresAt: new Date
78 email: userInfo.email
79 })
80 .onDuplicateKeyUpdate({
81 set: {
82 accessToken: encrypt(tokens
83 refreshToken: encrypt(tokens
84 expiresAt: new Date
85 email: userInfo.email
86 },
87 });
88 return fail("connected
89 } catch {
90 return fail("error
91 }
92}
from
"
drizzle-orm
"
;
NextRequest
}
from
"
next/server
"
;
from
"
@struxa/auth
"
;
from
"
@struxa/db
"
;
from
"
@struxa/db
"
;
from
"
@struxa/api/lib/crypto
"
;
NextResponse
)
{
(
"
gdrive_oauth_state
"
,
""
,
{
(
req
:
NextRequest
)
{
await
getAuth
()
;
await
auth
.
api
.
getSession
(
{
headers
:
req
.
headers
}
)
;
return
new
Response
(
"
Unauthorized
"
,
{
status
:
401
}
)
;
await
getAppUrl
()
;
new
URL
(
req
.
url
)
;
(
query
:
string
)
=>
{
NextResponse
.
redirect
(
`
${
appUrl
}
/account?gdrive=
${
query
}
`
)
;
.
get
(
"
error
"
))
return
fail
(
"
denied
"
)
;
url
.
searchParams
.
get
(
"
state
"
)
;
req
.
cookies
.
get
(
"
gdrive_oauth_state
"
)
?.
value
;
.
length
&&
(
state
)
,
Buffer
.
from
(
cookie
))
;
{
new
NextResponse
(
"
Invalid OAuth state
"
,
{
status
:
400
}
)
;
url
.
searchParams
.
get
(
"
code
"
)
;
return
fail
(
"
error
"
)
;
await
getOperatorGDriveConfig
()
;
return
fail
(
"
error
"
)
;
await
exchangeAuthorizationCode
(
await
fetchUserInfo
(
tokens
.
accessToken
)
;
await
db
.
query
.
userGoogleDrives
.
findFirst
(
{
.
userId
,
session
.
user
.
id
)
,
existing
.
email
!==
userInfo
.
email
)
{
"
)
;
.
id
,
.
accessToken
)
,
.
refreshToken
)
,
(
Date
.
now
()
+
tokens
.
expiresIn
*
1000
)
,
,
.
accessToken
)
,
.
refreshToken
)
,
(
Date
.
now
()
+
tokens
.
expiresIn
*
1000
)
,
,
"
)
;
"
)
;