Skip to content

Commit 9b5c03e

Browse files
committed
fix: google oauth token exchange using wrong content-type
1 parent f92c3c0 commit 9b5c03e

File tree

2 files changed

+88
-73
lines changed

2 files changed

+88
-73
lines changed

src/app.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ declare global {
55
DB: D1Database;
66
GITHUB_CLIENT_ID: string;
77
GITHUB_CLIENT_SECRET: string;
8+
GOOGLE_CLIENT_ID: string;
9+
GOOGLE_CLIENT_SECRET: string;
810
JWT_SECRET: string;
911
APP_URL: string;
1012
};

src/routes/api/auth/callback/google/+server.ts

Lines changed: 86 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -21,85 +21,98 @@ export const GET: RequestHandler = async ({ url, platform, cookies }) => {
2121
redirect(302, '/login?error=invalid_state');
2222
}
2323

24-
const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {
25-
method: 'POST',
26-
headers: { 'Content-Type': 'application/json' },
27-
body: JSON.stringify({
28-
client_id: env.GOOGLE_CLIENT_ID,
29-
client_secret: env.GOOGLE_CLIENT_SECRET,
30-
code,
31-
redirect_uri: `${env.APP_URL}/api/auth/callback/google`,
32-
grant_type: 'authorization_code'
33-
})
34-
});
35-
36-
const tokenData = await tokenResponse.json();
37-
if (tokenData.error || !tokenData.access_token) {
38-
redirect(302, '/login?error=token_failed');
39-
}
40-
41-
const userResponse = await fetch(GOOGLE_USERINFO_URL, {
42-
headers: { Authorization: `Bearer ${tokenData.access_token}` }
43-
});
44-
45-
const googleUser = await userResponse.json();
46-
if (!googleUser.id || !googleUser.email) {
47-
redirect(302, '/login?error=user_failed');
48-
}
49-
50-
const userId = `google_${googleUser.id}`;
51-
const reservedUsernames = ['openboot', 'admin', 'api', 'dashboard', 'install', 'login', 'logout', 'settings', 'help', 'support', 'docs', 'blog'];
52-
53-
let username = slugify(googleUser.email.split('@')[0]);
54-
if (reservedUsernames.includes(username.toLowerCase()) || username.length < 3) {
55-
username = `user-${googleUser.id.slice(-8)}`;
56-
}
57-
58-
const existingUser = await env.DB.prepare('SELECT username FROM users WHERE id = ?').bind(userId).first();
59-
if (existingUser) {
60-
username = (existingUser as { username: string }).username;
61-
}
24+
try {
25+
const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {
26+
method: 'POST',
27+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
28+
body: new URLSearchParams({
29+
client_id: env.GOOGLE_CLIENT_ID,
30+
client_secret: env.GOOGLE_CLIENT_SECRET,
31+
code,
32+
redirect_uri: `${env.APP_URL}/api/auth/callback/google`,
33+
grant_type: 'authorization_code'
34+
})
35+
});
36+
37+
const tokenData = await tokenResponse.json();
38+
if (tokenData.error || !tokenData.access_token) {
39+
redirect(302, '/login?error=token_failed');
40+
}
41+
42+
const userResponse = await fetch(GOOGLE_USERINFO_URL, {
43+
headers: { Authorization: `Bearer ${tokenData.access_token}` }
44+
});
45+
46+
const googleUser = await userResponse.json();
47+
if (!googleUser.id || !googleUser.email) {
48+
redirect(302, '/login?error=user_failed');
49+
}
50+
51+
const userId = `google_${googleUser.id}`;
52+
const reservedUsernames = ['openboot', 'admin', 'api', 'dashboard', 'install', 'login', 'logout', 'settings', 'help', 'support', 'docs', 'blog'];
53+
54+
let username = slugify(googleUser.email.split('@')[0]);
55+
if (reservedUsernames.includes(username.toLowerCase()) || username.length < 3) {
56+
username = `user-${googleUser.id.slice(-8)}`;
57+
}
58+
59+
const existingUser = await env.DB.prepare('SELECT username FROM users WHERE id = ?').bind(userId).first();
60+
if (existingUser) {
61+
username = (existingUser as { username: string }).username;
62+
} else {
63+
const usernameTaken = await env.DB.prepare('SELECT id FROM users WHERE username = ?').bind(username).first();
64+
if (usernameTaken) {
65+
username = `${username}-${googleUser.id.slice(-6)}`;
66+
}
67+
}
6268

63-
await env.DB.prepare(
64-
`
65-
INSERT INTO users (id, username, email, avatar_url, updated_at)
66-
VALUES (?, ?, ?, ?, datetime('now'))
67-
ON CONFLICT(id) DO UPDATE SET
68-
email = excluded.email,
69-
avatar_url = excluded.avatar_url,
70-
updated_at = datetime('now')
71-
`
72-
)
73-
.bind(userId, username, googleUser.email, googleUser.picture || '')
74-
.run();
75-
76-
const existingConfig = await env.DB.prepare('SELECT id FROM configs WHERE user_id = ? AND slug = ?').bind(userId, 'default').first();
77-
78-
if (!existingConfig) {
7969
await env.DB.prepare(
8070
`
81-
INSERT INTO configs (id, user_id, slug, name, description, base_preset, packages)
82-
VALUES (?, ?, 'default', 'Default', 'My default configuration', 'developer', '[]')
71+
INSERT INTO users (id, username, email, avatar_url, updated_at)
72+
VALUES (?, ?, ?, ?, datetime('now'))
73+
ON CONFLICT(id) DO UPDATE SET
74+
email = excluded.email,
75+
avatar_url = excluded.avatar_url,
76+
updated_at = datetime('now')
8377
`
8478
)
85-
.bind(generateId(), userId)
79+
.bind(userId, username, googleUser.email, googleUser.picture || '')
8680
.run();
87-
}
88-
89-
const thirtyDays = 30 * 24 * 60 * 60;
90-
const token = await signToken({ userId, username, exp: Date.now() + thirtyDays * 1000 }, env.JWT_SECRET);
9181

92-
cookies.set('session', token, {
93-
path: '/',
94-
httpOnly: true,
95-
secure: true,
96-
sameSite: 'lax',
97-
maxAge: thirtyDays
98-
});
82+
const existingConfig = await env.DB.prepare('SELECT id FROM configs WHERE user_id = ? AND slug = ?').bind(userId, 'default').first();
9983

100-
const returnTo = cookies.get('auth_return_to') || '/dashboard';
101-
cookies.delete('auth_state', { path: '/' });
102-
cookies.delete('auth_return_to', { path: '/' });
103-
104-
redirect(302, returnTo);
84+
if (!existingConfig) {
85+
await env.DB.prepare(
86+
`
87+
INSERT INTO configs (id, user_id, slug, name, description, base_preset, packages)
88+
VALUES (?, ?, 'default', 'Default', 'My default configuration', 'developer', '[]')
89+
`
90+
)
91+
.bind(generateId(), userId)
92+
.run();
93+
}
94+
95+
const thirtyDays = 30 * 24 * 60 * 60;
96+
const token = await signToken({ userId, username, exp: Date.now() + thirtyDays * 1000 }, env.JWT_SECRET);
97+
98+
cookies.set('session', token, {
99+
path: '/',
100+
httpOnly: true,
101+
secure: true,
102+
sameSite: 'lax',
103+
maxAge: thirtyDays
104+
});
105+
106+
const returnTo = cookies.get('auth_return_to') || '/dashboard';
107+
cookies.delete('auth_state', { path: '/' });
108+
cookies.delete('auth_return_to', { path: '/' });
109+
110+
redirect(302, returnTo);
111+
} catch (err) {
112+
if (err && typeof err === 'object' && 'status' in err && 'location' in err) {
113+
throw err;
114+
}
115+
console.error('Google auth callback error:', err);
116+
redirect(302, '/login?error=server_error');
117+
}
105118
};

0 commit comments

Comments
 (0)