Skip to content

Commit 936bd09

Browse files
committed
Fix GitHub OAuth in iframe
1 parent d4829c2 commit 936bd09

15 files changed

Lines changed: 464 additions & 86 deletions

File tree

app/api/auth/callback/vercel/route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { OAuth2Client, type OAuth2Tokens } from 'arctic'
33
import { getAppBaseUrl } from '@/lib/auth/oauth'
44
import { createSession, saveSession } from '@/lib/session/create'
55
import { cookies } from 'next/headers'
6+
import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy'
67

78
export async function GET(req: NextRequest): Promise<Response> {
89
const code = req.nextUrl.searchParams.get('code')
910
const state = req.nextUrl.searchParams.get('state')
1011
const cookieStore = await cookies()
12+
const authCookiePolicy = getAuthCookiePolicyFromRequest(req)
1113
const storedState = cookieStore.get(`vercel_oauth_state`)?.value ?? null
1214
const storedVerifier = cookieStore.get(`vercel_oauth_code_verifier`)?.value ?? null
1315
const storedRedirectTo = cookieStore.get(`vercel_oauth_redirect_to`)?.value ?? null
@@ -34,8 +36,8 @@ export async function GET(req: NextRequest): Promise<Response> {
3436

3537
try {
3638
tokens = await client.validateAuthorizationCode('https://vercel.com/api/login/oauth/token', code, storedVerifier)
37-
} catch (error) {
38-
console.error('Failed to validate authorization code:', error)
39+
} catch {
40+
console.error('Failed to validate authorization code')
3941
return new Response(null, {
4042
status: 400,
4143
})
@@ -61,7 +63,7 @@ export async function GET(req: NextRequest): Promise<Response> {
6163

6264
// Note: Vercel tokens are already stored in users table by upsertUser() in createSession()
6365

64-
await saveSession(response, session)
66+
await saveSession(response, session, authCookiePolicy)
6567

6668
cookieStore.delete(`vercel_oauth_state`)
6769
cookieStore.delete(`vercel_oauth_code_verifier`)

app/api/auth/github/callback/route.ts

Lines changed: 112 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { type NextRequest } from 'next/server'
22
import { cookies } from 'next/headers'
33
import { db } from '@/lib/db/client'
44
import { users, accounts, tasks, connectors, keys } from '@/lib/db/schema'
5-
import { eq, and } from 'drizzle-orm'
5+
import { eq, and, inArray } from 'drizzle-orm'
66
import { getAppBaseUrl, getGitHubClientId } from '@/lib/auth/oauth'
77
import { createGitHubSession, saveSession } from '@/lib/session/create-github'
88
import { encrypt } from '@/lib/crypto'
99
import { generateId } from '@/lib/utils/id'
10+
import { planUserKeyMerge } from '@/lib/auth/account-merge'
11+
import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy'
1012
import {
13+
GITHUB_AUTH_BROADCAST_CHANNEL,
1114
GITHUB_AUTH_ERROR_MESSAGE_TYPE,
1215
GITHUB_AUTH_POPUP_COOKIE,
1316
GITHUB_AUTH_POPUP_VALUE,
@@ -37,6 +40,7 @@ function cleanupGitHubAuthCookies(cookieStore: CookieStore): void {
3740
function createGitHubPopupResponse(req: NextRequest, status: PopupStatus, responseInit?: ResponseInit): Response {
3841
const origin = new URL(getAppBaseUrl(req)).origin
3942
const messageType = status === 'success' ? GITHUB_AUTH_SUCCESS_MESSAGE_TYPE : GITHUB_AUTH_ERROR_MESSAGE_TYPE
43+
const closeDelayMs = 250
4044
const html = `<!doctype html>
4145
<html>
4246
<head>
@@ -45,8 +49,32 @@ function createGitHubPopupResponse(req: NextRequest, status: PopupStatus, respon
4549
</head>
4650
<body>
4751
<script>
48-
window.opener?.postMessage({ type: ${JSON.stringify(messageType)}, status: ${JSON.stringify(status)} }, ${JSON.stringify(origin)});
49-
window.close();
52+
const githubAuthMessage = { type: ${JSON.stringify(messageType)}, status: ${JSON.stringify(status)} };
53+
const githubAuthOrigin = ${JSON.stringify(origin)};
54+
55+
function postGitHubAuthMessage(target) {
56+
try {
57+
target?.postMessage(githubAuthMessage, githubAuthOrigin);
58+
} catch {}
59+
}
60+
61+
postGitHubAuthMessage(window.opener);
62+
63+
try {
64+
if (window.opener?.frames) {
65+
for (let index = 0; index < window.opener.frames.length; index += 1) {
66+
postGitHubAuthMessage(window.opener.frames[index]);
67+
}
68+
}
69+
} catch {}
70+
71+
try {
72+
const channel = new BroadcastChannel(${JSON.stringify(GITHUB_AUTH_BROADCAST_CHANNEL)});
73+
channel.postMessage(githubAuthMessage);
74+
channel.close();
75+
} catch {}
76+
77+
window.setTimeout(() => window.close(), ${closeDelayMs});
5078
</script>
5179
</body>
5280
</html>`
@@ -65,6 +93,7 @@ export async function GET(req: NextRequest): Promise<Response> {
6593
const code = req.nextUrl.searchParams.get('code')
6694
const state = req.nextUrl.searchParams.get('state')
6795
const cookieStore = await cookies()
96+
const authCookiePolicy = getAuthCookiePolicyFromRequest(req)
6897

6998
const popupCookie = cookieStore.get(GITHUB_AUTH_POPUP_COOKIE)?.value ?? null
7099
if (popupCookie !== GITHUB_AUTH_POPUP_VALUE) {
@@ -161,13 +190,14 @@ export async function GET(req: NextRequest): Promise<Response> {
161190
}
162191

163192
const response = createGitHubPopupResponse(req, 'success')
164-
await saveSession(response, session)
193+
await saveSession(response, session, authCookiePolicy)
165194
cleanupGitHubAuthCookies(cookieStore)
166195

167196
return response
168197
}
169198

170199
const encryptedToken = encrypt(tokenData.access_token)
200+
const targetUserId = storedUserId!
171201

172202
const existingAccount = await db
173203
.select()
@@ -178,48 +208,106 @@ export async function GET(req: NextRequest): Promise<Response> {
178208
if (existingAccount.length > 0) {
179209
const connectedUserId = existingAccount[0].userId
180210

181-
if (connectedUserId !== storedUserId) {
211+
if (connectedUserId !== targetUserId) {
182212
console.info('GitHub OAuth account merge started')
183213

184-
await db.update(tasks).set({ userId: storedUserId! }).where(eq(tasks.userId, connectedUserId))
185-
await db.update(connectors).set({ userId: storedUserId! }).where(eq(connectors.userId, connectedUserId))
186-
await db.update(accounts).set({ userId: storedUserId! }).where(eq(accounts.userId, connectedUserId))
187-
await db.update(keys).set({ userId: storedUserId! }).where(eq(keys.userId, connectedUserId))
188-
await db.delete(users).where(eq(users.id, connectedUserId))
214+
await db.transaction(async (tx) => {
215+
await tx.update(tasks).set({ userId: targetUserId }).where(eq(tasks.userId, connectedUserId))
216+
await tx.update(connectors).set({ userId: targetUserId }).where(eq(connectors.userId, connectedUserId))
217+
218+
const [targetAccount] = await tx
219+
.select({ id: accounts.id })
220+
.from(accounts)
221+
.where(and(eq(accounts.userId, targetUserId), eq(accounts.provider, 'github')))
222+
.limit(1)
223+
224+
const sourceKeys = await tx
225+
.select({ id: keys.id, provider: keys.provider })
226+
.from(keys)
227+
.where(eq(keys.userId, connectedUserId))
228+
const targetKeys = await tx
229+
.select({ id: keys.id, provider: keys.provider })
230+
.from(keys)
231+
.where(eq(keys.userId, targetUserId))
232+
const keyMergePlan = planUserKeyMerge({ sourceKeys, targetKeys })
233+
234+
if (keyMergePlan.moveKeyIds.length > 0) {
235+
await tx.update(keys).set({ userId: targetUserId }).where(inArray(keys.id, keyMergePlan.moveKeyIds))
236+
}
237+
238+
if (keyMergePlan.deleteKeyIds.length > 0) {
239+
await tx.delete(keys).where(inArray(keys.id, keyMergePlan.deleteKeyIds))
240+
}
241+
242+
if (targetAccount) {
243+
await tx
244+
.update(accounts)
245+
.set({
246+
accessToken: encryptedToken,
247+
externalUserId: `${githubUser.id}`,
248+
scope: tokenData.scope,
249+
username: githubUser.login,
250+
updatedAt: new Date(),
251+
})
252+
.where(eq(accounts.id, targetAccount.id))
253+
await tx.delete(accounts).where(eq(accounts.id, existingAccount[0].id))
254+
} else {
255+
await tx
256+
.update(accounts)
257+
.set({
258+
userId: targetUserId,
259+
accessToken: encryptedToken,
260+
scope: tokenData.scope,
261+
username: githubUser.login,
262+
updatedAt: new Date(),
263+
})
264+
.where(eq(accounts.id, existingAccount[0].id))
265+
}
266+
267+
await tx.delete(users).where(eq(users.id, connectedUserId))
268+
})
189269

190270
console.info('GitHub OAuth account merge completed')
191-
271+
} else {
192272
await db
193273
.update(accounts)
194274
.set({
195-
userId: storedUserId!,
196275
accessToken: encryptedToken,
197276
scope: tokenData.scope,
198277
username: githubUser.login,
199278
updatedAt: new Date(),
200279
})
201280
.where(eq(accounts.id, existingAccount[0].id))
202-
} else {
281+
}
282+
} else {
283+
const [currentAccount] = await db
284+
.select({ id: accounts.id })
285+
.from(accounts)
286+
.where(and(eq(accounts.userId, targetUserId), eq(accounts.provider, 'github')))
287+
.limit(1)
288+
289+
if (currentAccount) {
203290
await db
204291
.update(accounts)
205292
.set({
293+
externalUserId: `${githubUser.id}`,
206294
accessToken: encryptedToken,
207295
scope: tokenData.scope,
208296
username: githubUser.login,
209297
updatedAt: new Date(),
210298
})
211-
.where(eq(accounts.id, existingAccount[0].id))
299+
.where(eq(accounts.id, currentAccount.id))
300+
} else {
301+
await db.insert(accounts).values({
302+
id: generateId(21),
303+
userId: targetUserId,
304+
provider: 'github',
305+
externalUserId: `${githubUser.id}`,
306+
accessToken: encryptedToken,
307+
scope: tokenData.scope,
308+
username: githubUser.login,
309+
})
212310
}
213-
} else {
214-
await db.insert(accounts).values({
215-
id: generateId(21),
216-
userId: storedUserId!,
217-
provider: 'github',
218-
externalUserId: `${githubUser.id}`,
219-
accessToken: encryptedToken,
220-
scope: tokenData.scope,
221-
username: githubUser.login,
222-
})
223311
}
224312

225313
cleanupGitHubAuthCookies(cookieStore)

app/api/auth/github/signin/route.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getSessionFromReq } from '@/lib/session/server'
44
import { GITHUB_OAUTH_SCOPE, getAppBaseUrl, getGitHubClientId } from '@/lib/auth/oauth'
55
import { isRelativeUrl } from '@/lib/utils/is-relative-url'
66
import { generateState } from 'arctic'
7+
import { getAuthCookiePolicyFromRequest, getAuthCookieSameSite, getAuthCookieSecure } from '@/lib/auth/cookie-policy'
78
import {
89
GITHUB_AUTH_POPUP_COOKIE,
910
GITHUB_AUTH_POPUP_PARAM,
@@ -12,13 +13,18 @@ import {
1213

1314
const GITHUB_AUTH_COOKIE_MAX_AGE = 60 * 10
1415

15-
function setGitHubAuthCookie(store: Awaited<ReturnType<typeof cookies>>, key: string, value: string): void {
16+
function setGitHubAuthCookie(
17+
store: Awaited<ReturnType<typeof cookies>>,
18+
key: string,
19+
value: string,
20+
authCookiePolicy: ReturnType<typeof getAuthCookiePolicyFromRequest>,
21+
): void {
1622
store.set(key, value, {
1723
path: '/',
18-
secure: process.env.NODE_ENV === 'production',
24+
secure: getAuthCookieSecure(authCookiePolicy),
1925
httpOnly: true,
2026
maxAge: GITHUB_AUTH_COOKIE_MAX_AGE,
21-
sameSite: 'lax',
27+
sameSite: getAuthCookieSameSite(authCookiePolicy),
2228
})
2329
}
2430

@@ -42,6 +48,7 @@ export async function GET(req: NextRequest): Promise<Response> {
4248

4349
const state = generateState()
4450
const store = await cookies()
51+
const authCookiePolicy = getAuthCookiePolicyFromRequest(req)
4552
const redirectTo = isRelativeUrl(req.nextUrl.searchParams.get('next') ?? '/')
4653
? (req.nextUrl.searchParams.get('next') ?? '/')
4754
: '/'
@@ -54,7 +61,7 @@ export async function GET(req: NextRequest): Promise<Response> {
5461
['github_auth_mode', 'connect'],
5562
['github_auth_user_id', session.user.id],
5663
]) {
57-
setGitHubAuthCookie(store, key, value)
64+
setGitHubAuthCookie(store, key, value, authCookiePolicy)
5865
}
5966

6067
// Build GitHub authorization URL
@@ -91,6 +98,7 @@ export async function POST(req: NextRequest): Promise<Response> {
9198

9299
const state = generateState()
93100
const store = await cookies()
101+
const authCookiePolicy = getAuthCookiePolicyFromRequest(req)
94102
const redirectTo = isRelativeUrl(req.nextUrl.searchParams.get('next') ?? '/')
95103
? (req.nextUrl.searchParams.get('next') ?? '/')
96104
: '/'
@@ -103,7 +111,7 @@ export async function POST(req: NextRequest): Promise<Response> {
103111
['github_auth_mode', 'connect'],
104112
['github_auth_user_id', session.user.id],
105113
]) {
106-
setGitHubAuthCookie(store, key, value)
114+
setGitHubAuthCookie(store, key, value, authCookiePolicy)
107115
}
108116

109117
// Build GitHub authorization URL

app/api/auth/info/route.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { createSession, saveSession } from '@/lib/session/create'
44
import { saveSession as saveGitHubSession } from '@/lib/session/create-github'
55
import { getSessionFromReq } from '@/lib/session/server'
66
import { getOAuthToken } from '@/lib/session/get-oauth-token'
7+
import { getAuthCookiePolicyFromRequest } from '@/lib/auth/cookie-policy'
78

89
export async function GET(req: NextRequest) {
910
const existingSession = await getSessionFromReq(req)
11+
const authCookiePolicy = getAuthCookiePolicyFromRequest(req)
1012

1113
// For GitHub users, just return the existing session without recreating it
1214
// For Vercel users, recreate the session to refresh user data
@@ -35,9 +37,9 @@ export async function GET(req: NextRequest) {
3537

3638
// Use the appropriate saveSession function based on auth provider
3739
if (session && session.authProvider === 'github') {
38-
await saveGitHubSession(response, session)
40+
await saveGitHubSession(response, session, authCookiePolicy)
3941
} else {
40-
await saveSession(response, session)
42+
await saveSession(response, session, authCookiePolicy)
4143
}
4244

4345
return response

0 commit comments

Comments
 (0)